diff --git a/Podfile b/Podfile new file mode 100644 index 00000000..d01acf46 --- /dev/null +++ b/Podfile @@ -0,0 +1,16 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'Weather' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for Weather + pod 'Kingfisher', '~> 4.0' + + target 'WeatherTests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/README.md b/README.md index b3fe3b4f..cd2981a5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ +Weather +====== + +This repository contains a sample Weather app for requesting Weather's API. + + +--- +# Installation + +To install the dependencies +* Open a terminal and cd to the directory containing the Podfile +* Run the `pod install` command + +(For further details regarding cocoapod installation see https://cocoapods.org/) + +Notes: +Don't forget to change the apiKey for the Weather's API in "Constants.swift" + +--- +# Existing Functionalities + +The app is currently able to load Weather data from Weather's API, and show it in 2 UI designs (list form and dashboard) + +* When the app loads, it will load the Weather data from Weather's API, and show them in list form ( like iOS default "Weather" app) +* Upon selecting "Other Design", it will open a view with Dashboard like presentation of Weather data + + +--- +# Development Steps + +1. Create new project based on single view app +2. Create folders for MVVM pattern +3. Add WeatherViewController and Design the UI layout to Weather data +4. Add LocationManager to detect user current location +5. Add Networking Layer to handle the Weather's API +6. Add ViewModel and Model, that will show the Weather data at WeatherViewController +7. Add Forecast and ForecastDaily features +8. Add scroll effect feature +9. Add pods: Kingfisher +10. Add MyWeatherViewController and Design the Dashboard UI layout +11. Implement existing ViewModels to MyWeatherViewController +10. Add Unit Test to test the process + + + + +================================================================== + + # Work sample - Application developer ## Assignment diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj new file mode 100644 index 00000000..5b1abcea --- /dev/null +++ b/Weather.xcodeproj/project.pbxproj @@ -0,0 +1,974 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 0590753E29507F710CF3CED8 /* Pods_WeatherTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */; }; + 158601774FA4161333DFB17B /* Pods_Weather.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */; }; + AA0BF2C421984CAF0093D62F /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2C321984CAF0093D62F /* UIColorExtension.swift */; }; + AA0BF2C8219854370093D62F /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2C7219854370093D62F /* XCTestCase.swift */; }; + AA0BF2CA219854640093D62F /* weather.json in Resources */ = {isa = PBXBuildFile; fileRef = AA0BF2C9219854640093D62F /* weather.json */; }; + AA0BF2CC2198547D0093D62F /* forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = AA0BF2CB2198547D0093D62F /* forecast.json */; }; + AA0BF2CE2198548F0093D62F /* forecastDaily.json in Resources */ = {isa = PBXBuildFile; fileRef = AA0BF2CD2198548F0093D62F /* forecastDaily.json */; }; + AA0BF2D0219855040093D62F /* WeatherViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2CF219855040093D62F /* WeatherViewModelTests.swift */; }; + AA0BF2D7219859130093D62F /* MockNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2D6219859130093D62F /* MockNetworkManager.swift */; }; + AA0E4B01218E940500379569 /* JSONParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */; }; + AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF5218E940500379569 /* ParameterEncoding.swift */; }; + AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */; }; + AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF8218E940500379569 /* WeatherEndPoint.swift */; }; + AA0E4B05218E940500379569 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFA218E940500379569 /* NetworkManager.swift */; }; + AA0E4B06218E940500379569 /* EndPointType.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFC218E940500379569 /* EndPointType.swift */; }; + AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFD218E940500379569 /* HTTPMethod.swift */; }; + AA0E4B08218E940500379569 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFE218E940500379569 /* HTTPTask.swift */; }; + AA0E4B09218E940500379569 /* NetworkLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFF218E940500379569 /* NetworkLogger.swift */; }; + AA0E4B0A218E940500379569 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4B00218E940500379569 /* Router.swift */; }; + AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A2218CE3D400A23A36 /* AppDelegate.swift */; }; + AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */; }; + AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A6218CE3D400A23A36 /* Main.storyboard */; }; + AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A9218CE3DD00A23A36 /* Assets.xcassets */; }; + AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; + AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; + AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA316FA2218DD78B00DD1794 /* LocationManager.swift */; }; + AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */; }; + AA38D29F2196D44900FAFFE3 /* MyWeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA38D29E2196D44900FAFFE3 /* MyWeatherViewController.swift */; }; + AA408126219785C400612F95 /* ForecastCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA408124219785C400612F95 /* ForecastCell.xib */; }; + AA4243BF2194DF140030A3B3 /* CoreImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243B92194DF130030A3B3 /* CoreImage.framework */; }; + AA4243C02194DF140030A3B3 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BA2194DF130030A3B3 /* libxml2.tbd */; }; + AA4243C12194DF140030A3B3 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BB2194DF130030A3B3 /* QuartzCore.framework */; }; + AA4243C22194DF140030A3B3 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BC2194DF130030A3B3 /* UIKit.framework */; }; + AA4243C32194DF140030A3B3 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BD2194DF140030A3B3 /* CoreGraphics.framework */; }; + AA4243C42194DF140030A3B3 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BE2194DF140030A3B3 /* CoreText.framework */; }; + AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */; }; + AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */; }; + AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9F2190E7D70069B282 /* StringExtension.swift */; }; + AA44BCA22190F2DD0069B282 /* ForecastDailyStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */; }; + AA44BCA42190F31B0069B282 /* ForecastDailyListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA32190F31B0069B282 /* ForecastDailyListViewModel.swift */; }; + AA44BCA62190F3700069B282 /* ForecastDailyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA52190F3700069B282 /* ForecastDailyViewModel.swift */; }; + AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; + AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; + AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; + AA4528CD218DA5FD00EC0901 /* ForecastCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CC218DA5FD00EC0901 /* ForecastCell.swift */; }; + AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CE218DB18400EC0901 /* InfoView.swift */; }; + AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528D0218DB19700EC0901 /* InfoView.xib */; }; + AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */; }; + AA7F66352198C5E40061DFE8 /* ForecastListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7F66342198C5E40061DFE8 /* ForecastListViewModelTests.swift */; }; + AA7F66372198CB960061DFE8 /* DateConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7F66362198CB960061DFE8 /* DateConverterTests.swift */; }; + AAC62752218EAF7600A221C0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC62751218EAF7600A221C0 /* Weather.swift */; }; + AAE4A44721923DF70068D8F1 /* DateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE4A44621923DF70068D8F1 /* DateConverter.swift */; }; + AAE8A3A22197E84700DF123F /* ForecastDailyCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */; }; + AAE8A3A42197EFCF00DF123F /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8A3A32197EFCF00DF123F /* UIViewExtension.swift */; }; + AAEF7DA1219808D400C5FDF8 /* WeatherDetailProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEF7DA0219808D400C5FDF8 /* WeatherDetailProtocol.swift */; }; + AAEF7DA32198301700C5FDF8 /* DayTimeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEF7DA22198301700C5FDF8 /* DayTimeColor.swift */; }; + AAF473082190804000D09B00 /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473072190804000D09B00 /* Forecast.swift */; }; + AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473092190830400D09B00 /* MiscInfo.swift */; }; + AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF4730B2190851300D09B00 /* ForecastDaily.swift */; }; + AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF64E1218EF84500C512B8 /* Dynamic.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AA3006B4218CE3DD00A23A36 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA300697218CE3D400A23A36 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA30069E218CE3D400A23A36; + remoteInfo = Weather; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + AA4243D02194DFF10030A3B3 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 8EFD89BC805FB41F6EE88D57 /* Pods-Weather.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Weather.release.xcconfig"; path = "Pods/Target Support Files/Pods-Weather/Pods-Weather.release.xcconfig"; sourceTree = ""; }; + 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WeatherTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests.debug.xcconfig"; sourceTree = ""; }; + 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WeatherTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests.release.xcconfig"; sourceTree = ""; }; + AA0BF2C321984CAF0093D62F /* UIColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; }; + AA0BF2C7219854370093D62F /* XCTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; + AA0BF2C9219854640093D62F /* weather.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = weather.json; sourceTree = ""; }; + AA0BF2CB2198547D0093D62F /* forecast.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = forecast.json; sourceTree = ""; }; + AA0BF2CD2198548F0093D62F /* forecastDaily.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = forecastDaily.json; sourceTree = ""; }; + AA0BF2CF219855040093D62F /* WeatherViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModelTests.swift; sourceTree = ""; }; + AA0BF2D6219859130093D62F /* MockNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkManager.swift; sourceTree = ""; }; + AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONParameterEncoder.swift; sourceTree = ""; }; + AA0E4AF5218E940500379569 /* ParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParameterEncoding.swift; sourceTree = ""; }; + AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLParameterEncoding.swift; sourceTree = ""; }; + AA0E4AF8218E940500379569 /* WeatherEndPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherEndPoint.swift; sourceTree = ""; }; + AA0E4AFA218E940500379569 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + AA0E4AFC218E940500379569 /* EndPointType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndPointType.swift; sourceTree = ""; }; + AA0E4AFD218E940500379569 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + AA0E4AFE218E940500379569 /* HTTPTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; + AA0E4AFF218E940500379569 /* NetworkLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkLogger.swift; sourceTree = ""; }; + AA0E4B00218E940500379569 /* Router.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + AA30069F218CE3D400A23A36 /* Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Weather.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AA3006A2218CE3D400A23A36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; sourceTree = ""; }; + AA3006A7218CE3D400A23A36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AA3006A9218CE3DD00A23A36 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AA3006AC218CE3DD00A23A36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AA3006AE218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; }; + AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA316FA2218DD78B00DD1794 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; + AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastListViewModel.swift; sourceTree = ""; }; + AA38D29E2196D44900FAFFE3 /* MyWeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyWeatherViewController.swift; sourceTree = ""; }; + AA408124219785C400612F95 /* ForecastCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ForecastCell.xib; sourceTree = ""; }; + AA4243B92194DF130030A3B3 /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = System/Library/Frameworks/CoreImage.framework; sourceTree = SDKROOT; }; + AA4243BA2194DF130030A3B3 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; + AA4243BB2194DF130030A3B3 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + AA4243BC2194DF130030A3B3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + AA4243BD2194DF140030A3B3 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + AA4243BE2194DF140030A3B3 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; + AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCollectionView.swift; sourceTree = ""; }; + AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = ""; }; + AA44BC9F2190E7D70069B282 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyStackView.swift; sourceTree = ""; }; + AA44BCA32190F31B0069B282 /* ForecastDailyListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyListViewModel.swift; sourceTree = ""; }; + AA44BCA52190F3700069B282 /* ForecastDailyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyViewModel.swift; sourceTree = ""; }; + AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; + AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; + AA4528CC218DA5FD00EC0901 /* ForecastCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCell.swift; sourceTree = ""; }; + AA4528CE218DB18400EC0901 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; + AA4528D0218DB19700EC0901 /* InfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoView.xib; sourceTree = ""; }; + AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; + AA7F66342198C5E40061DFE8 /* ForecastListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastListViewModelTests.swift; sourceTree = ""; }; + AA7F66362198CB960061DFE8 /* DateConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverterTests.swift; sourceTree = ""; }; + AAC62751218EAF7600A221C0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; + AAE4A44621923DF70068D8F1 /* DateConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverter.swift; sourceTree = ""; }; + AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyCollectionView.swift; sourceTree = ""; }; + AAE8A3A32197EFCF00DF123F /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; + AAEF7DA0219808D400C5FDF8 /* WeatherDetailProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDetailProtocol.swift; sourceTree = ""; }; + AAEF7DA22198301700C5FDF8 /* DayTimeColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayTimeColor.swift; sourceTree = ""; }; + AAF473072190804000D09B00 /* Forecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Forecast.swift; sourceTree = ""; }; + AAF473092190830400D09B00 /* MiscInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiscInfo.swift; sourceTree = ""; }; + AAF4730B2190851300D09B00 /* ForecastDaily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDaily.swift; sourceTree = ""; }; + AAFF64E1218EF84500C512B8 /* Dynamic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dynamic.swift; sourceTree = ""; }; + B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Weather.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WeatherTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FB3BD15C15224C07D1446635 /* Pods-Weather.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Weather.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Weather/Pods-Weather.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AA30069C218CE3D400A23A36 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AA4243BF2194DF140030A3B3 /* CoreImage.framework in Frameworks */, + AA4243C02194DF140030A3B3 /* libxml2.tbd in Frameworks */, + AA4243C12194DF140030A3B3 /* QuartzCore.framework in Frameworks */, + AA4243C22194DF140030A3B3 /* UIKit.framework in Frameworks */, + AA4243C32194DF140030A3B3 /* CoreGraphics.framework in Frameworks */, + AA4243C42194DF140030A3B3 /* CoreText.framework in Frameworks */, + 158601774FA4161333DFB17B /* Pods_Weather.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3006B0218CE3DD00A23A36 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0590753E29507F710CF3CED8 /* Pods_WeatherTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 789780D72854A0B8E4D49467 /* Pods */ = { + isa = PBXGroup; + children = ( + FB3BD15C15224C07D1446635 /* Pods-Weather.debug.xcconfig */, + 8EFD89BC805FB41F6EE88D57 /* Pods-Weather.release.xcconfig */, + 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */, + 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AA0BF2C5219853EA0093D62F /* Resource */ = { + isa = PBXGroup; + children = ( + AA0BF2C9219854640093D62F /* weather.json */, + AA0BF2CB2198547D0093D62F /* forecast.json */, + AA0BF2CD2198548F0093D62F /* forecastDaily.json */, + ); + path = Resource; + sourceTree = ""; + }; + AA0BF2C6219853FC0093D62F /* Extension */ = { + isa = PBXGroup; + children = ( + AA0BF2C7219854370093D62F /* XCTestCase.swift */, + ); + path = Extension; + sourceTree = ""; + }; + AA0BF2D5219858F40093D62F /* MockService */ = { + isa = PBXGroup; + children = ( + AA0BF2D6219859130093D62F /* MockNetworkManager.swift */, + ); + path = MockService; + sourceTree = ""; + }; + AA0E4AF2218E940500379569 /* Networking */ = { + isa = PBXGroup; + children = ( + AA0E4AF3218E940500379569 /* Encoding */, + AA0E4AF7218E940500379569 /* EndPoint */, + AA0E4AF9218E940500379569 /* Manager */, + AA0E4AFB218E940500379569 /* Service */, + ); + path = Networking; + sourceTree = ""; + }; + AA0E4AF3218E940500379569 /* Encoding */ = { + isa = PBXGroup; + children = ( + AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */, + AA0E4AF5218E940500379569 /* ParameterEncoding.swift */, + AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */, + ); + path = Encoding; + sourceTree = ""; + }; + AA0E4AF7218E940500379569 /* EndPoint */ = { + isa = PBXGroup; + children = ( + AA0E4AF8218E940500379569 /* WeatherEndPoint.swift */, + ); + path = EndPoint; + sourceTree = ""; + }; + AA0E4AF9218E940500379569 /* Manager */ = { + isa = PBXGroup; + children = ( + AA0E4AFA218E940500379569 /* NetworkManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + AA0E4AFB218E940500379569 /* Service */ = { + isa = PBXGroup; + children = ( + AA0E4AFC218E940500379569 /* EndPointType.swift */, + AA0E4AFD218E940500379569 /* HTTPMethod.swift */, + AA0E4AFE218E940500379569 /* HTTPTask.swift */, + AA0E4AFF218E940500379569 /* NetworkLogger.swift */, + AA0E4B00218E940500379569 /* Router.swift */, + ); + path = Service; + sourceTree = ""; + }; + AA300696218CE3D400A23A36 = { + isa = PBXGroup; + children = ( + AA3006A1218CE3D400A23A36 /* Weather */, + AA3006B6218CE3DD00A23A36 /* WeatherTests */, + AA3006A0218CE3D400A23A36 /* Products */, + AA4243B82194DF130030A3B3 /* Frameworks */, + 789780D72854A0B8E4D49467 /* Pods */, + ); + sourceTree = ""; + }; + AA3006A0218CE3D400A23A36 /* Products */ = { + isa = PBXGroup; + children = ( + AA30069F218CE3D400A23A36 /* Weather.app */, + AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AA3006A1218CE3D400A23A36 /* Weather */ = { + isa = PBXGroup; + children = ( + AA3006C3218CF08D00A23A36 /* Service */, + AA3006C2218CF07F00A23A36 /* Utils */, + AA3006A2218CE3D400A23A36 /* AppDelegate.swift */, + AA3006A6218CE3D400A23A36 /* Main.storyboard */, + AA3006A9218CE3DD00A23A36 /* Assets.xcassets */, + AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */, + AA3006AE218CE3DD00A23A36 /* Info.plist */, + AA4528C4218D9EEB00EC0901 /* Constants.swift */, + AA3006C4218CF09B00A23A36 /* Model */, + AA3006C5218CF0A300A23A36 /* View */, + AA3006C6218CF0BC00A23A36 /* ViewModel */, + ); + path = Weather; + sourceTree = ""; + }; + AA3006B6218CE3DD00A23A36 /* WeatherTests */ = { + isa = PBXGroup; + children = ( + AA0BF2C5219853EA0093D62F /* Resource */, + AA0BF2C6219853FC0093D62F /* Extension */, + AA0BF2D5219858F40093D62F /* MockService */, + AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */, + AA3006B9218CE3DD00A23A36 /* Info.plist */, + AA0BF2CF219855040093D62F /* WeatherViewModelTests.swift */, + AA7F66342198C5E40061DFE8 /* ForecastListViewModelTests.swift */, + AA7F66362198CB960061DFE8 /* DateConverterTests.swift */, + ); + path = WeatherTests; + sourceTree = ""; + }; + AA3006C2218CF07F00A23A36 /* Utils */ = { + isa = PBXGroup; + children = ( + AA44BC9E2190E7C10069B282 /* Extension */, + AAE4A44621923DF70068D8F1 /* DateConverter.swift */, + AAEF7DA22198301700C5FDF8 /* DayTimeColor.swift */, + ); + path = Utils; + sourceTree = ""; + }; + AA3006C3218CF08D00A23A36 /* Service */ = { + isa = PBXGroup; + children = ( + AA0E4AF2218E940500379569 /* Networking */, + AA316FA2218DD78B00DD1794 /* LocationManager.swift */, + ); + path = Service; + sourceTree = ""; + }; + AA3006C4218CF09B00A23A36 /* Model */ = { + isa = PBXGroup; + children = ( + AAFF64E1218EF84500C512B8 /* Dynamic.swift */, + AAC62751218EAF7600A221C0 /* Weather.swift */, + AAF473072190804000D09B00 /* Forecast.swift */, + AAF4730B2190851300D09B00 /* ForecastDaily.swift */, + AAF473092190830400D09B00 /* MiscInfo.swift */, + ); + path = Model; + sourceTree = ""; + }; + AA3006C5218CF0A300A23A36 /* View */ = { + isa = PBXGroup; + children = ( + AA4528C9218D9FAE00EC0901 /* Cell */, + AA4528C8218D9F9C00EC0901 /* Custom */, + AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, + AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */, + AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */, + AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */, + AA38D29E2196D44900FAFFE3 /* MyWeatherViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + AA3006C6218CF0BC00A23A36 /* ViewModel */ = { + isa = PBXGroup; + children = ( + AAEF7DA0219808D400C5FDF8 /* WeatherDetailProtocol.swift */, + AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */, + AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */, + AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */, + AA44BCA32190F31B0069B282 /* ForecastDailyListViewModel.swift */, + AA44BCA52190F3700069B282 /* ForecastDailyViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + AA4243B82194DF130030A3B3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AA4243BD2194DF140030A3B3 /* CoreGraphics.framework */, + AA4243B92194DF130030A3B3 /* CoreImage.framework */, + AA4243BE2194DF140030A3B3 /* CoreText.framework */, + AA4243BA2194DF130030A3B3 /* libxml2.tbd */, + AA4243BB2194DF130030A3B3 /* QuartzCore.framework */, + AA4243BC2194DF130030A3B3 /* UIKit.framework */, + B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */, + DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + AA44BC9E2190E7C10069B282 /* Extension */ = { + isa = PBXGroup; + children = ( + AA44BC9F2190E7D70069B282 /* StringExtension.swift */, + AAE8A3A32197EFCF00DF123F /* UIViewExtension.swift */, + AA0BF2C321984CAF0093D62F /* UIColorExtension.swift */, + ); + path = Extension; + sourceTree = ""; + }; + AA4528C8218D9F9C00EC0901 /* Custom */ = { + isa = PBXGroup; + children = ( + AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */, + AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */, + AA4528CE218DB18400EC0901 /* InfoView.swift */, + AA4528D0218DB19700EC0901 /* InfoView.xib */, + ); + path = Custom; + sourceTree = ""; + }; + AA4528C9218D9FAE00EC0901 /* Cell */ = { + isa = PBXGroup; + children = ( + AA4528CC218DA5FD00EC0901 /* ForecastCell.swift */, + AA408124219785C400612F95 /* ForecastCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AA30069E218CE3D400A23A36 /* Weather */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA3006BC218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "Weather" */; + buildPhases = ( + C59C8281D6BBDBB7CC7893B8 /* [CP] Check Pods Manifest.lock */, + AA30069B218CE3D400A23A36 /* Sources */, + AA30069C218CE3D400A23A36 /* Frameworks */, + AA30069D218CE3D400A23A36 /* Resources */, + AA4243D02194DFF10030A3B3 /* Embed Frameworks */, + AD01839882209B46949BE2F3 /* [CP] Embed Pods Frameworks */, + 2EC49454CADDC3B0895C0223 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Weather; + productName = Weather; + productReference = AA30069F218CE3D400A23A36 /* Weather.app */; + productType = "com.apple.product-type.application"; + }; + AA3006B2218CE3DD00A23A36 /* WeatherTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA3006BF218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "WeatherTests" */; + buildPhases = ( + EE1C1927C10FAB21B0C98122 /* [CP] Check Pods Manifest.lock */, + AA3006AF218CE3DD00A23A36 /* Sources */, + AA3006B0218CE3DD00A23A36 /* Frameworks */, + AA3006B1218CE3DD00A23A36 /* Resources */, + 0D4E8826DC9F48538C7E9A22 /* [CP] Embed Pods Frameworks */, + 17BD24C2EA7815C78DC25A5F /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + AA3006B5218CE3DD00A23A36 /* PBXTargetDependency */, + ); + name = WeatherTests; + productName = WeatherTests; + productReference = AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AA300697218CE3D400A23A36 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; + ORGANIZATIONNAME = "Christian Huang"; + TargetAttributes = { + AA30069E218CE3D400A23A36 = { + CreatedOnToolsVersion = 10.0; + }; + AA3006B2218CE3DD00A23A36 = { + CreatedOnToolsVersion = 10.0; + TestTargetID = AA30069E218CE3D400A23A36; + }; + }; + }; + buildConfigurationList = AA30069A218CE3D400A23A36 /* Build configuration list for PBXProject "Weather" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AA300696218CE3D400A23A36; + productRefGroup = AA3006A0218CE3D400A23A36 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AA30069E218CE3D400A23A36 /* Weather */, + AA3006B2218CE3DD00A23A36 /* WeatherTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AA30069D218CE3D400A23A36 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */, + AA408126219785C400612F95 /* ForecastCell.xib in Resources */, + AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */, + AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */, + AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */, + AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3006B1218CE3DD00A23A36 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0BF2CC2198547D0093D62F /* forecast.json in Resources */, + AA0BF2CA219854640093D62F /* weather.json in Resources */, + AA0BF2CE2198548F0093D62F /* forecastDaily.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0D4E8826DC9F48538C7E9A22 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 17BD24C2EA7815C78DC25A5F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2EC49454CADDC3B0895C0223 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Weather/Pods-Weather-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + AD01839882209B46949BE2F3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Weather/Pods-Weather-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Weather/Pods-Weather-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C59C8281D6BBDBB7CC7893B8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Weather-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EE1C1927C10FAB21B0C98122 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-WeatherTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AA30069B218CE3D400A23A36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0E4B05218E940500379569 /* NetworkManager.swift in Sources */, + AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */, + AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */, + AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */, + AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */, + AAE8A3A22197E84700DF123F /* ForecastDailyCollectionView.swift in Sources */, + AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */, + AAEF7DA1219808D400C5FDF8 /* WeatherDetailProtocol.swift in Sources */, + AA0BF2C421984CAF0093D62F /* UIColorExtension.swift in Sources */, + AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */, + AAE8A3A42197EFCF00DF123F /* UIViewExtension.swift in Sources */, + AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */, + AA0E4B06218E940500379569 /* EndPointType.swift in Sources */, + AAC62752218EAF7600A221C0 /* Weather.swift in Sources */, + AA0E4B01218E940500379569 /* JSONParameterEncoder.swift in Sources */, + AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */, + AA0E4B08218E940500379569 /* HTTPTask.swift in Sources */, + AA38D29F2196D44900FAFFE3 /* MyWeatherViewController.swift in Sources */, + AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */, + AA0E4B09218E940500379569 /* NetworkLogger.swift in Sources */, + AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */, + AA0E4B0A218E940500379569 /* Router.swift in Sources */, + AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */, + AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */, + AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */, + AAE4A44721923DF70068D8F1 /* DateConverter.swift in Sources */, + AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, + AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */, + AA44BCA22190F2DD0069B282 /* ForecastDailyStackView.swift in Sources */, + AA44BCA62190F3700069B282 /* ForecastDailyViewModel.swift in Sources */, + AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, + AA4528CD218DA5FD00EC0901 /* ForecastCell.swift in Sources */, + AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */, + AAF473082190804000D09B00 /* Forecast.swift in Sources */, + AAEF7DA32198301700C5FDF8 /* DayTimeColor.swift in Sources */, + AA44BCA42190F31B0069B282 /* ForecastDailyListViewModel.swift in Sources */, + AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3006AF218CE3DD00A23A36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA7F66372198CB960061DFE8 /* DateConverterTests.swift in Sources */, + AA0BF2C8219854370093D62F /* XCTestCase.swift in Sources */, + AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */, + AA0BF2D7219859130093D62F /* MockNetworkManager.swift in Sources */, + AA7F66352198C5E40061DFE8 /* ForecastListViewModelTests.swift in Sources */, + AA0BF2D0219855040093D62F /* WeatherViewModelTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AA3006B5218CE3DD00A23A36 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA30069E218CE3D400A23A36 /* Weather */; + targetProxy = AA3006B4218CE3DD00A23A36 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + AA3006A6218CE3D400A23A36 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AA3006A7218CE3D400A23A36 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AA3006AC218CE3DD00A23A36 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AA3006BA218CE3DD00A23A36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AA3006BB218CE3DD00A23A36 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AA3006BD218CE3DD00A23A36 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB3BD15C15224C07D1446635 /* Pods-Weather.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)/CocoaLumberjack", + ); + INFOPLIST_FILE = Weather/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.Weather; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AA3006BE218CE3DD00A23A36 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8EFD89BC805FB41F6EE88D57 /* Pods-Weather.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)/CocoaLumberjack", + ); + INFOPLIST_FILE = Weather/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.Weather; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AA3006C0218CE3DD00A23A36 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + INFOPLIST_FILE = WeatherTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.WeatherTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Weather.app/Weather"; + }; + name = Debug; + }; + AA3006C1218CE3DD00A23A36 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + INFOPLIST_FILE = WeatherTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.WeatherTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Weather.app/Weather"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AA30069A218CE3D400A23A36 /* Build configuration list for PBXProject "Weather" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3006BA218CE3DD00A23A36 /* Debug */, + AA3006BB218CE3DD00A23A36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA3006BC218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "Weather" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3006BD218CE3DD00A23A36 /* Debug */, + AA3006BE218CE3DD00A23A36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA3006BF218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "WeatherTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3006C0218CE3DD00A23A36 /* Debug */, + AA3006C1218CE3DD00A23A36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AA300697218CE3D400A23A36 /* Project object */; +} diff --git a/Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..561f654e --- /dev/null +++ b/Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..5ab3c8a6 --- /dev/null +++ b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + Weather.xcscheme + + orderHint + 0 + + Weather.xcscheme_^#shared#^_ + + orderHint + 3 + + + + diff --git a/Weather.xcworkspace/contents.xcworkspacedata b/Weather.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..71d6a2f2 --- /dev/null +++ b/Weather.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Weather/AppDelegate.swift b/Weather/AppDelegate.swift new file mode 100644 index 00000000..21c65efc --- /dev/null +++ b/Weather/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/Weather/Assets.xcassets/AppIcon.appiconset/Contents.json b/Weather/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/Weather/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Weather/Assets.xcassets/Contents.json b/Weather/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Weather/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Weather/Base.lproj/LaunchScreen.storyboard b/Weather/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..3eb85391 --- /dev/null +++ b/Weather/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard new file mode 100644 index 00000000..8c624f98 --- /dev/null +++ b/Weather/Base.lproj/Main.storyboard @@ -0,0 +1,779 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/Constants.swift b/Weather/Constants.swift new file mode 100644 index 00000000..31262d28 --- /dev/null +++ b/Weather/Constants.swift @@ -0,0 +1,15 @@ +// +// Constants.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct Constants { + static let apiKey = "" + static let networkEnvironment: NetworkEnvironment = .production +} diff --git a/Weather/Info.plist b/Weather/Info.plist new file mode 100644 index 00000000..c11238e0 --- /dev/null +++ b/Weather/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSLocationWhenInUseUsageDescription + This app need to access your location, for Weather Forecasting. + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Weather/Model/Dynamic.swift b/Weather/Model/Dynamic.swift new file mode 100755 index 00000000..d38266c8 --- /dev/null +++ b/Weather/Model/Dynamic.swift @@ -0,0 +1,29 @@ +// +// Dynamic.swift +// ZenJob +// +// + +class Dynamic { + typealias Listener = (T) -> () + var listener: Listener? + + func bind(_ listener: Listener?) { + self.listener = listener + } + + func bindAndFire(_ listener: Listener?) { + self.listener = listener + listener?(value) + } + + var value: T { + didSet { + listener?(value) + } + } + + init(_ v: T) { + value = v + } +} diff --git a/Weather/Model/Forecast.swift b/Weather/Model/Forecast.swift new file mode 100644 index 00000000..d96c1268 --- /dev/null +++ b/Weather/Model/Forecast.swift @@ -0,0 +1,33 @@ +// +// Forecast.swift +// Weather +// +// Created by Christian Huang on 05/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct ForecastResponse: Codable { + let city: City + let list: [Forecast] +} + +struct Forecast: Codable { + let dayTime: TimeInterval + let main: WeatherData + let weather: [Weather] + let clouds: Clouds + let wind: Wind + let dayTimeText: String + + enum CodingKeys: String, CodingKey { + case dayTime = "dt" + case main + case weather + case clouds + case wind + case dayTimeText = "dt_txt" + } +} diff --git a/Weather/Model/ForecastDaily.swift b/Weather/Model/ForecastDaily.swift new file mode 100644 index 00000000..dc8ddfb3 --- /dev/null +++ b/Weather/Model/ForecastDaily.swift @@ -0,0 +1,39 @@ +// +// ForecastDaily.swift +// Weather +// +// Created by Christian Huang on 05/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct ForecastDailyResponse: Codable { + let city: City + let list: [ForecastDaily] +} + +struct ForecastDaily: Codable { + let dayTime: TimeInterval + let temperature: Temperature + let pressure: Double + let humidity: Int + let weather: [Weather] + let speed: Double + let degree: Double + let clouds: Double + let rain: Double? + + enum CodingKeys: String, CodingKey { + case dayTime = "dt" + case temperature = "temp" + case pressure + case humidity + case weather + case speed + case degree = "deg" + case clouds + case rain + } +} diff --git a/Weather/Model/MiscInfo.swift b/Weather/Model/MiscInfo.swift new file mode 100644 index 00000000..a34e268f --- /dev/null +++ b/Weather/Model/MiscInfo.swift @@ -0,0 +1,98 @@ +// +// Info.swift +// Weather +// +// Created by Christian Huang on 05/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct Coordinate: Codable { + let longitude: Double + let latitude: Double + + enum CodingKeys: String, CodingKey { + case longitude = "lon" + case latitude = "lat" + } +} + +struct Temperature: Codable { + let min: Double + let max: Double + let morning: Double + let day: Double + let evening: Double + let night: Double + + enum CodingKeys: String, CodingKey { + case min + case max + case morning = "morn" + case day + case evening = "eve" + case night + } +} + +struct Wind: Codable { + let speed: Double + let degree: Double? + + enum CodingKeys: String, CodingKey { + case speed + case degree = "deg" + } +} + +struct Clouds: Codable { + let cloudiness: Double + + enum CodingKeys: String, CodingKey { + case cloudiness = "all" + } +} + +struct Rain: Codable { + let rain3h: Double + + enum CodingKeys: String, CodingKey { + case rain3h = "3h" + } +} + +struct Snow: Codable { + let snow3h: Double + + enum CodingKeys: String, CodingKey { + case snow3h = "3h" + } +} + +struct SystemData: Codable { + let type: Int + let id: Int + let message: Double + let country: String + let sunrise: TimeInterval + let sunset: TimeInterval + +} + +struct City: Codable { + let id: Int + let name: String + let coordinate: Coordinate + let country: String + + enum CodingKeys: String, CodingKey { + case id + case name + case coordinate = "coord" + case country + } +} + + diff --git a/Weather/Model/Weather.swift b/Weather/Model/Weather.swift new file mode 100644 index 00000000..f042cabc --- /dev/null +++ b/Weather/Model/Weather.swift @@ -0,0 +1,64 @@ +// +// Weather.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + +struct WeatherResponse: Codable { + let coordinate: Coordinate + let weather: [Weather] + let base: String + let main: WeatherData + let wind: Wind + let clouds: Clouds + //let rain: Rain? + //let snow: Snow? + let dayTime: TimeInterval + let system: SystemData + let id: Int + let name: String + + enum CodingKeys: String, CodingKey { + case coordinate = "coord" + case weather + case base + case main + case wind + case clouds + case dayTime = "dt" + case system = "sys" + case id + case name + } +} + +struct Weather: Codable { + let id: Int + let main: String + let description: String + let icon: String +} + +struct WeatherData: Codable { + let temperature: Double + let pressure: Double + let humidity: Int + let temperatureMin: Double + let temperatureMax: Double + let seaLevel: Double? + let groundLevel: Double? + + enum CodingKeys: String, CodingKey { + case temperature = "temp" + case pressure + case humidity + case temperatureMin = "temp_min" + case temperatureMax = "temp_max" + case seaLevel = "sea_level" + case groundLevel = "grnd_level" + } +} diff --git a/Weather/Service/LocationManager.swift b/Weather/Service/LocationManager.swift new file mode 100644 index 00000000..d064f71c --- /dev/null +++ b/Weather/Service/LocationManager.swift @@ -0,0 +1,101 @@ +// +// LocationManager.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit +import CoreLocation + +class LocationManager: NSObject { + static private var shared: LocationManager! + private var locationManager = CLLocationManager() + var lastLocation: CLLocation? + var lastPlacemark: CLPlacemark? + var isPlacemarkNeeded = false + var locationUpdateClosure: ((CLLocation?, CLPlacemark?)->())? + + static func sharedInstance() -> LocationManager { + if shared == nil { + shared = LocationManager() + } + return shared + } + + func startUpdatingLocation(placemarkNeeded: Bool = false) { + isPlacemarkNeeded = placemarkNeeded + if self.requestAuthorization() { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.delegate = self + locationManager.startUpdatingLocation() + } + } + + func stopUpdatingLocation() { + locationManager.stopUpdatingLocation() + } + + func requestLocation(placemarkNeeded: Bool = false) { + isPlacemarkNeeded = placemarkNeeded + if self.requestAuthorization() { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.delegate = self + locationManager.requestLocation() + } + } + + func lookUpLocation(_ location: CLLocation, completionHandler: @escaping (CLPlacemark?) -> Void ) { + + let geocoder = CLGeocoder() + + // Look up the location and pass it to the completion handler + geocoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in + if error == nil { + let firstLocation = placemarks?[0] + completionHandler(firstLocation) + } + else { + // An error occurred during geocoding. + completionHandler(nil) + } + }) + } +} + +//MARK:- private func +extension LocationManager { + private func requestAuthorization() -> Bool { + if CLLocationManager.locationServicesEnabled() == true { + if CLLocationManager.authorizationStatus() == .restricted || CLLocationManager.authorizationStatus() == .denied || CLLocationManager.authorizationStatus() == .notDetermined { + locationManager.requestWhenInUseAuthorization() + } + return true + } else { + print("Please turn on location services or GPS") + return false + } + } +} + +//MARK:- CLLocationManager Delegates +extension LocationManager: CLLocationManagerDelegate { + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + lastLocation = locations[0] + if isPlacemarkNeeded { + lookUpLocation(lastLocation!) { placemark in + self.lastPlacemark = placemark + self.locationUpdateClosure?(locations[0], placemark) + } + } + + locationUpdateClosure?(lastLocation, nil) + + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Unable to access your current location: \(error)") + } +} diff --git a/Weather/Service/Networking/Encoding/JSONParameterEncoder.swift b/Weather/Service/Networking/Encoding/JSONParameterEncoder.swift new file mode 100755 index 00000000..7cf4ba40 --- /dev/null +++ b/Weather/Service/Networking/Encoding/JSONParameterEncoder.swift @@ -0,0 +1,22 @@ +// +// JSONEncoding.swift +// NetworkLayer +// +// + +import Foundation + +public struct JSONParameterEncoder: ParameterEncoder { + public func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws { + do { + let jsonAsData = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) + urlRequest.httpBody = jsonAsData + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + }catch { + throw NetworkError.encodingFailed + } + } +} + diff --git a/Weather/Service/Networking/Encoding/ParameterEncoding.swift b/Weather/Service/Networking/Encoding/ParameterEncoding.swift new file mode 100755 index 00000000..05556fc6 --- /dev/null +++ b/Weather/Service/Networking/Encoding/ParameterEncoding.swift @@ -0,0 +1,52 @@ +// +// ParameterEncoding.swift +// NetworkLayer +// +// + +import Foundation + +public typealias Parameters = [String:Any] + +public protocol ParameterEncoder { + func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws +} + +public enum ParameterEncoding { + + case urlEncoding + case jsonEncoding + case urlAndJsonEncoding + + public func encode(urlRequest: inout URLRequest, + bodyParameters: Parameters?, + urlParameters: Parameters?) throws { + do { + switch self { + case .urlEncoding: + guard let urlParameters = urlParameters else { return } + try URLParameterEncoder().encode(urlRequest: &urlRequest, with: urlParameters) + + case .jsonEncoding: + guard let bodyParameters = bodyParameters else { return } + try JSONParameterEncoder().encode(urlRequest: &urlRequest, with: bodyParameters) + + case .urlAndJsonEncoding: + guard let bodyParameters = bodyParameters, + let urlParameters = urlParameters else { return } + try URLParameterEncoder().encode(urlRequest: &urlRequest, with: urlParameters) + try JSONParameterEncoder().encode(urlRequest: &urlRequest, with: bodyParameters) + + } + }catch { + throw error + } + } +} + + +public enum NetworkError : String, Error { + case parametersNil = "Parameters were nil." + case encodingFailed = "Parameter encoding failed." + case missingURL = "URL is nil." +} diff --git a/Weather/Service/Networking/Encoding/URLParameterEncoding.swift b/Weather/Service/Networking/Encoding/URLParameterEncoding.swift new file mode 100755 index 00000000..5e92bdcb --- /dev/null +++ b/Weather/Service/Networking/Encoding/URLParameterEncoding.swift @@ -0,0 +1,32 @@ +// +// URLEncoding.swift +// NetworkLayer +// +// + +import Foundation + +public struct URLParameterEncoder: ParameterEncoder { + public func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws { + + guard let url = urlRequest.url else { throw NetworkError.missingURL } + + if var urlComponents = URLComponents(url: url, + resolvingAgainstBaseURL: false), !parameters.isEmpty { + + urlComponents.queryItems = [URLQueryItem]() + + for (key,value) in parameters { + let queryItem = URLQueryItem(name: key, + value: "\(value)") //.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + urlComponents.queryItems?.append(queryItem) + } + urlRequest.url = urlComponents.url + } + + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") + } + + } +} diff --git a/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift b/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift new file mode 100755 index 00000000..6a93f78a --- /dev/null +++ b/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift @@ -0,0 +1,81 @@ +// +// WeatherEndPoint.swift +// NetworkLayer +// +// + +import Foundation + +public enum WeatherAPI { + case weather(latitude: Float, longitude: Float, apiKey: String) + case forecast(latitude: Float, longitude: Float, apiKey: String) + case forecastDaily(latitude: Float, longitude: Float, apiKey: String) +} + +extension WeatherAPI: EndPointType { + /** API base urls. */ + var environmentBaseURL : String { + switch NetworkManager.environment { + case .production: return "http://worksample-api.herokuapp.com/" + default: return "http://worksample-api.herokuapp.com/" + } + } + + var baseURL: URL { + guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} + return url + } + /** API path for specific request. */ + var path: String { + switch self { + case .weather: + return "weather" + case .forecast: + return "forecast" + case .forecastDaily: + return "forecast/daily" + } + + } + + var httpMethod: HTTPMethod { + switch self { + default: + return .get + } + } + + /** generate task based on requested Weather's API. */ + var task: HTTPTask { + switch self { + case .weather(let latitude, let longitude, let apiKey): + return .requestParametersAndHeaders(bodyParameters: nil, + bodyEncoding: .urlEncoding, + urlParameters: ["lat" : latitude, + "lon" : longitude, + "key" : apiKey], + additionHeaders: nil) + case .forecast(let latitude, let longitude, let apiKey): + return .requestParametersAndHeaders(bodyParameters: nil, + bodyEncoding: .urlEncoding, + urlParameters: ["lat" : latitude, + "lon" : longitude, + "key" : apiKey], + additionHeaders: nil) + case .forecastDaily(let latitude, let longitude, let apiKey): + return .requestParametersAndHeaders(bodyParameters: nil, + bodyEncoding: .urlEncoding, + urlParameters: ["lat" : latitude, + "lon" : longitude, + "key" : apiKey], + additionHeaders: nil) + } + } + + var headers: HTTPHeaders? { + return nil + } +} + + + diff --git a/Weather/Service/Networking/Manager/NetworkManager.swift b/Weather/Service/Networking/Manager/NetworkManager.swift new file mode 100755 index 00000000..72e31298 --- /dev/null +++ b/Weather/Service/Networking/Manager/NetworkManager.swift @@ -0,0 +1,147 @@ +// +// NetworkManager.swift +// NetworkLayer +// +// + +import Foundation + +enum NetworkResponse:String { + case success + case authenticationError = "network_authentication_error" + case badRequest = "network_bad_request" + case outdated = "network_outdated" + case failed = "network_failed" + case noData = "network_no_data" + case unableToDecode = "network_unable_to_decode" + case noResponseObject = "network_no_response_object" +} + +enum Result{ + case success + case failure(String) +} + +protocol WeatherNetworkManager { + var apiKey: String { get set } + + init(apiKey: String, environment: NetworkEnvironment) + /** + Fetch weather data from Weather API server + - Parameter latitude: user's latitude position. + - Parameter longitude: user's longitude position. + - Parameter completion: block to handle the fetch results + */ + func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (_ response: WeatherResponse?,_ error: String?)->()) + + func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastResponse?,_ error: String?)->()) + + func fetchForecastDaily(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastDailyResponse?,_ error: String?)->()) +} + +class NetworkManager: WeatherNetworkManager { + static var environment : NetworkEnvironment = .production + var apiKey: String + let router = Router() + + required init(apiKey: String, environment: NetworkEnvironment) { + self.apiKey = apiKey + NetworkManager.environment = environment + } + + /** + Fetch weather data from Weather API server + - Parameter lat: user's latitude position. + - Parameter lon: user's longitude position. + - Parameter completion: block to handle the fetch results + */ + func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (_ response: WeatherResponse?,_ error: String?)->()) { + router.request(.weather(latitude: Float(latitude), longitude: Float(longitude), apiKey: apiKey)) { data, response, error in + let result = self.handleNetworkResponse(data: data, response: response, error: error) + switch result{ + case .success: + do { + /*let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) + print(jsonData)*/ + let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: data!) + completion(weatherResponse, nil) + }catch { + print(error) + completion(nil, NetworkResponse.unableToDecode.rawValue) + } + case .failure(let error): + completion(nil, error) + } + } + } + + func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastResponse?,_ error: String?)->()) { + router.request(.forecast(latitude: Float(latitude), longitude: Float(longitude), apiKey: apiKey)) { data, response, error in + let result = self.handleNetworkResponse(data: data, response: response, error: error) + switch result{ + case .success: + do { + /*let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) + print(jsonData)*/ + let forecastResponse = try JSONDecoder().decode(ForecastResponse.self, from: data!) + completion(forecastResponse, nil) + }catch { + print(error) + completion(nil, NetworkResponse.unableToDecode.rawValue) + } + case .failure(let error): + completion(nil, error) + } + } + } + + func fetchForecastDaily(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastDailyResponse?,_ error: String?)->()) { + router.request(.forecastDaily(latitude: Float(latitude), longitude: Float(longitude), apiKey: apiKey)) { data, response, error in + let result = self.handleNetworkResponse(data: data, response: response, error: error) + switch result{ + case .success: + do { + /*let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) + print(jsonData)*/ + let forecastDailyResponse = try JSONDecoder().decode(ForecastDailyResponse.self, from: data!) + completion(forecastDailyResponse, nil) + }catch { + print(error) + completion(nil, NetworkResponse.unableToDecode.rawValue) + } + case .failure(let error): + completion(nil, error) + } + } + } + + func cancelLastRequest() { + router.cancel() + } + + /** + Handle all basic network error responses + */ + fileprivate func handleNetworkResponse(data: Data?, response: URLResponse?, error: Error?) -> Result{ + if error != nil { + return .failure("Please check your network connection.") + } + + if let response = response as? HTTPURLResponse { + switch response.statusCode { + case 200...299: + print("HTTPURLResponse: success") + case 401...500: return .failure(NetworkResponse.authenticationError.rawValue) + case 501...599: return .failure(NetworkResponse.badRequest.rawValue) + case 600: return .failure(NetworkResponse.outdated.rawValue) + default: return .failure(NetworkResponse.failed.rawValue) + } + } + + guard data != nil else { + return .failure(NetworkResponse.noData.rawValue) + } + + return .success + } +} diff --git a/Weather/Service/Networking/Service/EndPointType.swift b/Weather/Service/Networking/Service/EndPointType.swift new file mode 100755 index 00000000..25a1ced2 --- /dev/null +++ b/Weather/Service/Networking/Service/EndPointType.swift @@ -0,0 +1,22 @@ +// +// EndPoint.swift +// NetworkLayer +// +// + +import Foundation + +enum NetworkEnvironment { + case qa + case production + case staging +} + +protocol EndPointType { + var baseURL: URL { get } + var path: String { get } + var httpMethod: HTTPMethod { get } + var task: HTTPTask { get } + var headers: HTTPHeaders? { get } +} + diff --git a/Weather/Service/Networking/Service/HTTPMethod.swift b/Weather/Service/Networking/Service/HTTPMethod.swift new file mode 100755 index 00000000..35db0736 --- /dev/null +++ b/Weather/Service/Networking/Service/HTTPMethod.swift @@ -0,0 +1,15 @@ +// +// HTTPMethod.swift +// NetworkLayer +// +// + +import Foundation + +public enum HTTPMethod : String { + case get = "GET" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" +} diff --git a/Weather/Service/Networking/Service/HTTPTask.swift b/Weather/Service/Networking/Service/HTTPTask.swift new file mode 100755 index 00000000..38de0b6e --- /dev/null +++ b/Weather/Service/Networking/Service/HTTPTask.swift @@ -0,0 +1,26 @@ +// +// HTTPTask.swift +// NetworkLayer +// +// + +import Foundation + +public typealias HTTPHeaders = [String:String] + +public enum HTTPTask { + case request + + case requestParameters(bodyParameters: Parameters?, + bodyEncoding: ParameterEncoding, + urlParameters: Parameters?) + + case requestParametersAndHeaders(bodyParameters: Parameters?, + bodyEncoding: ParameterEncoding, + urlParameters: Parameters?, + additionHeaders: HTTPHeaders?) + + // case download, upload...etc +} + + diff --git a/Weather/Service/Networking/Service/NetworkLogger.swift b/Weather/Service/Networking/Service/NetworkLogger.swift new file mode 100755 index 00000000..86e28389 --- /dev/null +++ b/Weather/Service/Networking/Service/NetworkLogger.swift @@ -0,0 +1,39 @@ +// +// NetworkLogger.swift +// NetworkLayer +// +// + +import Foundation + +class NetworkLogger { + static func log(request: URLRequest) { + + print("\n - - - - - - - - - - OUTGOING - - - - - - - - - - \n") + defer { print("\n - - - - - - - - - - END - - - - - - - - - - \n") } + + let urlAsString = request.url?.absoluteString ?? "" + let urlComponents = NSURLComponents(string: urlAsString) + + let method = request.httpMethod != nil ? "\(request.httpMethod ?? "")" : "" + let path = "\(urlComponents?.path ?? "")" + let query = "\(urlComponents?.query ?? "")" + let host = "\(urlComponents?.host ?? "")" + + var logOutput = """ + \(urlAsString) \n\n + \(method) \(path)?\(query) HTTP/1.1 \n + HOST: \(host)\n + """ + for (key,value) in request.allHTTPHeaderFields ?? [:] { + logOutput += "\(key): \(value) \n" + } + if let body = request.httpBody { + logOutput += "\n \(NSString(data: body, encoding: String.Encoding.utf8.rawValue) ?? "")" + } + + print(logOutput) + } + + static func log(response: URLResponse) {} +} diff --git a/Weather/Service/Networking/Service/Router.swift b/Weather/Service/Networking/Service/Router.swift new file mode 100755 index 00000000..1a1e8da7 --- /dev/null +++ b/Weather/Service/Networking/Service/Router.swift @@ -0,0 +1,96 @@ +// +// NetworkService.swift +// NetworkLayer +// +// + +import Foundation + +public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() + +protocol NetworkRouter: class { + associatedtype EndPoint: EndPointType + func request(_ route: EndPoint, completion: @escaping NetworkRouterCompletion) + func cancel() +} + +class Router: NetworkRouter { + private let session = URLSession(configuration: .default) + private var task: URLSessionTask? + + func request(_ route: EndPoint, completion: @escaping NetworkRouterCompletion) { + //let session = URLSession.shared + do { + let request = try self.buildRequest(from: route) + NetworkLogger.log(request: request) + task = session.dataTask(with: request, completionHandler: { data, response, error in + completion(data, response, error) + }) + }catch { + completion(nil, nil, error) + } + self.task?.resume() + } + + func cancel() { + self.task?.cancel() + } + + fileprivate func buildRequest(from route: EndPoint) throws -> URLRequest { + + var request = URLRequest(url: route.baseURL.appendingPathComponent(route.path), + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: 10.0) + + request.httpMethod = route.httpMethod.rawValue + do { + switch route.task { + case .request: + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + case .requestParameters(let bodyParameters, + let bodyEncoding, + let urlParameters): + + try self.configureParameters(bodyParameters: bodyParameters, + bodyEncoding: bodyEncoding, + urlParameters: urlParameters, + request: &request) + + case .requestParametersAndHeaders(let bodyParameters, + let bodyEncoding, + let urlParameters, + let additionalHeaders): + + self.addAdditionalHeaders(additionalHeaders, request: &request) + try self.configureParameters(bodyParameters: bodyParameters, + bodyEncoding: bodyEncoding, + urlParameters: urlParameters, + request: &request) + } + return request + } catch { + throw error + } + } + + fileprivate func configureParameters(bodyParameters: Parameters?, + bodyEncoding: ParameterEncoding, + urlParameters: Parameters?, + request: inout URLRequest) throws { + do { + try bodyEncoding.encode(urlRequest: &request, + bodyParameters: bodyParameters, urlParameters: urlParameters) + } catch { + throw error + } + } + + fileprivate func addAdditionalHeaders(_ additionalHeaders: HTTPHeaders?, request: inout URLRequest) { + guard let headers = additionalHeaders else { return } + for (key, value) in headers { + request.setValue(value, forHTTPHeaderField: key) + } + } + +} + diff --git a/Weather/Utils/DateConverter.swift b/Weather/Utils/DateConverter.swift new file mode 100644 index 00000000..063d9dfb --- /dev/null +++ b/Weather/Utils/DateConverter.swift @@ -0,0 +1,71 @@ +// +// DateConverter.swift +// Weather +// +// Created by Christian Huang on 07/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class DateConverter { + + static func timeIntervalToDayString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE" + return dateFormatter.string(from: date) + } + static func timeIntervalToDayShortString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EE" + return dateFormatter.string(from: date) + } + + static func timeIntervalToHourString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH" + return dateFormatter.string(from: date) + } + + static func timeIntervalToHourMinuteString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH':'mm" + return dateFormatter.string(from: date) + } + + static func timeIntervalToDayTimeInterval(_ time: TimeInterval) -> TimeInterval{ + let date = Date(timeIntervalSince1970: time) + let calendar = Calendar.current + let components = calendar.dateComponents([.hour, .minute], from: date) + return TimeInterval(((components.hour! * 60) + components.minute!) * 60) + } + + static func stringToDate(dateString: String, timezone: String = "CEST") -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZ" + dateFormatter.timeZone = TimeZone(abbreviation: timezone) + let date = dateFormatter.date(from: dateString) + return date + } + + static func dateToDateString(date: Date, timezone: String = "CEST") -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E. dd.MM" + dateFormatter.timeZone = TimeZone(abbreviation: timezone) + + return dateFormatter.string(from:date) + } + + static func dateToTimeString(date: Date, timezone: String = "CEST") -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "hh.mm" + dateFormatter.timeZone = TimeZone(abbreviation: timezone) + + return dateFormatter.string(from:date) + } +} diff --git a/Weather/Utils/DayTimeColor.swift b/Weather/Utils/DayTimeColor.swift new file mode 100644 index 00000000..b0b41284 --- /dev/null +++ b/Weather/Utils/DayTimeColor.swift @@ -0,0 +1,32 @@ +// +// DayTimeColor.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + + +enum DayTime: Int { + case morning + case day + case evening + case night +} + +class DayTimeColor { + static func colorFor(_ dayTime: DayTime) -> UIColor { + switch dayTime { + case .morning: + return UIColor(hexString: "D9781F") + case .day: + return UIColor(hexString: "499BC4") + case .evening: + return UIColor(hexString: "C43900") + default: + return UIColor.black + } + } +} diff --git a/Weather/Utils/Extension/StringExtension.swift b/Weather/Utils/Extension/StringExtension.swift new file mode 100644 index 00000000..6d81b7f5 --- /dev/null +++ b/Weather/Utils/Extension/StringExtension.swift @@ -0,0 +1,51 @@ +// +// StringExtension.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + +extension String { + subscript(value: NSRange) -> Substring { + return self[value.lowerBound..) -> Substring { + get { + return self[index(at: value.lowerBound)...index(at: value.upperBound)] + } + } + + subscript(value: CountableRange) -> Substring { + get { + return self[index(at: value.lowerBound)..) -> Substring { + get { + return self[..) -> Substring { + get { + return self[...index(at: value.upperBound)] + } + } + + subscript(value: PartialRangeFrom) -> Substring { + get { + return self[index(at: value.lowerBound)...] + } + } + + func index(at offset: Int) -> String.Index { + return index(startIndex, offsetBy: offset) + } +} diff --git a/Weather/Utils/Extension/UIColorExtension.swift b/Weather/Utils/Extension/UIColorExtension.swift new file mode 100644 index 00000000..e5b0c63e --- /dev/null +++ b/Weather/Utils/Extension/UIColorExtension.swift @@ -0,0 +1,39 @@ +// +// UIColorExtension.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +extension UIColor { + convenience init(hexString: String, alpha: CGFloat = 1.0) { + let hexString: String = hexString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + let scanner = Scanner(string: hexString) + if (hexString.hasPrefix("#")) { + scanner.scanLocation = 1 + } + var color: UInt32 = 0 + scanner.scanHexInt32(&color) + let mask = 0x000000FF + let r = Int(color >> 16) & mask + let g = Int(color >> 8) & mask + let b = Int(color) & mask + let red = CGFloat(r) / 255.0 + let green = CGFloat(g) / 255.0 + let blue = CGFloat(b) / 255.0 + self.init(red:red, green:green, blue:blue, alpha:alpha) + } + + func toHexString() -> String { + var r:CGFloat = 0 + var g:CGFloat = 0 + var b:CGFloat = 0 + var a:CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 + return String(format:"#%06x", rgb) + } +} diff --git a/Weather/Utils/Extension/UIViewExtension.swift b/Weather/Utils/Extension/UIViewExtension.swift new file mode 100644 index 00000000..f6457796 --- /dev/null +++ b/Weather/Utils/Extension/UIViewExtension.swift @@ -0,0 +1,19 @@ +// +// UIViewExtension.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + + +//MARK:- layer related +extension UIView { + func setFullRoundedCorner(borderWidth: CGFloat = 0, borderColor: CGColor = UIColor.clear.cgColor) { + self.layer.cornerRadius = self.bounds.height / 2 + self.layer.borderWidth = borderWidth + self.layer.borderColor = borderColor + } +} diff --git a/Weather/View/Cell/ForecastCell.swift b/Weather/View/Cell/ForecastCell.swift new file mode 100644 index 00000000..628d4693 --- /dev/null +++ b/Weather/View/Cell/ForecastCell.swift @@ -0,0 +1,37 @@ +// +// ForecastCell.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit +import Kingfisher + +class ForecastCell: UICollectionViewCell { + @IBOutlet private weak var timeLabel: UILabel! + @IBOutlet private weak var chanceLabel: UILabel! + @IBOutlet private weak var weatherImageView: UIImageView! + @IBOutlet private weak var temperatureLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + func configureCell(forecastViewModel: ForecastViewModel, isNow: Bool = false) { + timeLabel.text = isNow ? "Now" : forecastViewModel.time + chanceLabel.text = nil + weatherImageView.kf.setImage(with: URL(string: forecastViewModel.icon)) + temperatureLabel.text = forecastViewModel.temperature + } + + func configureCell(forecastDailyViewModel: ForecastDailyViewModel, isNow: Bool = false) { + timeLabel.text = isNow ? "Now" : forecastDailyViewModel.dayShortString + chanceLabel.text = nil + weatherImageView.kf.setImage(with: URL(string: forecastDailyViewModel.icon)) + temperatureLabel.text = "\(Int(forecastDailyViewModel.temperatureMin))°-\(Int(forecastDailyViewModel.temperatureMax))°" + } + +} diff --git a/Weather/View/Cell/ForecastCell.xib b/Weather/View/Cell/ForecastCell.xib new file mode 100644 index 00000000..5dab3158 --- /dev/null +++ b/Weather/View/Cell/ForecastCell.xib @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/View/Custom/DailyForecastView.swift b/Weather/View/Custom/DailyForecastView.swift new file mode 100644 index 00000000..f20c1845 --- /dev/null +++ b/Weather/View/Custom/DailyForecastView.swift @@ -0,0 +1,51 @@ +// +// DailyForecastView.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class DailyForecastView: UIView { + @IBOutlet private weak var dayLabel: UILabel! + @IBOutlet private weak var todayLabel: UILabel! + @IBOutlet private weak var weatherImageView: UIImageView! + @IBOutlet private weak var maxLabel: UILabel! + @IBOutlet private weak var minLabel: UILabel! + + + class func instanceFromNib() -> DailyForecastView { + let instance = UINib(nibName: "DailyForecastView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! DailyForecastView + instance.translatesAutoresizingMaskIntoConstraints = false + instance.heightAnchor.constraint(equalToConstant: instance.bounds.height).isActive = true + return instance + } + + /* + // Only override draw() if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + override func draw(_ rect: CGRect) { + // Drawing code + } + */ + + func configureTodayView(weatherViewModel: WeatherViewModel) { + dayLabel.text = weatherViewModel.dayString + todayLabel.alpha = 1 + weatherImageView.image = nil + maxLabel.text = "\(Int(weatherViewModel.temperatureMax))°" + minLabel.text = "\(Int(weatherViewModel.temperatureMin))°" + } + + func configureTodayView(forecastDailyViewModel: ForecastDailyViewModel) { + + dayLabel.text = forecastDailyViewModel.dayString + todayLabel.alpha = 0 + weatherImageView.kf.setImage(with: URL(string: forecastDailyViewModel.icon)) + maxLabel.text = "\(Int(forecastDailyViewModel.temperatureMax))°" + minLabel.text = "\(Int(forecastDailyViewModel.temperatureMin))°" + } + +} diff --git a/Weather/View/Custom/DailyForecastView.xib b/Weather/View/Custom/DailyForecastView.xib new file mode 100644 index 00000000..1adc6c4a --- /dev/null +++ b/Weather/View/Custom/DailyForecastView.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/View/Custom/InfoView.swift b/Weather/View/Custom/InfoView.swift new file mode 100644 index 00000000..3fe09dbe --- /dev/null +++ b/Weather/View/Custom/InfoView.swift @@ -0,0 +1,49 @@ +// +// InfoView.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class InfoView: UIView { + var detail1: String = "" { + didSet{ + detail1Label.text = detail1 + } + } + var detail2: String = "" { + didSet{ + detail2Label.text = detail2 + } + } + @IBOutlet private weak var title1Label: UILabel! + @IBOutlet private weak var detail1Label: UILabel! + @IBOutlet private weak var title2Label: UILabel! + @IBOutlet private weak var detail2Label: UILabel! + @IBOutlet private weak var separatorView: UIView! + + class func instanceFromNib() -> InfoView { + let instance = UINib(nibName: "InfoView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! InfoView + instance.translatesAutoresizingMaskIntoConstraints = false + instance.heightAnchor.constraint(equalToConstant: instance.bounds.height).isActive = true + return instance + } + + func configureInfoView(title1: String, title2: String, hasSeparator: Bool = true) { + title1Label.text = title1 + title2Label.text = title2 + separatorView.alpha = hasSeparator ? 1 : 0 + } + + /* + // Only override draw() if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + override func draw(_ rect: CGRect) { + // Drawing code + } + */ + +} diff --git a/Weather/View/Custom/InfoView.xib b/Weather/View/Custom/InfoView.xib new file mode 100644 index 00000000..a37b3d38 --- /dev/null +++ b/Weather/View/Custom/InfoView.xib @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/View/ForecastCollectionView.swift b/Weather/View/ForecastCollectionView.swift new file mode 100644 index 00000000..f39eb99b --- /dev/null +++ b/Weather/View/ForecastCollectionView.swift @@ -0,0 +1,54 @@ +// +// ForecastCollectionView.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastCollectionView: UICollectionView { + + var forecastListViewModel = ForecastListViewModel() + + override func awakeFromNib() { + super.awakeFromNib() + self.register(UINib(nibName: "ForecastCell", bundle: nil), forCellWithReuseIdentifier: "forecastCellIdentifier") + self.dataSource = self + self.delegate = self + } + +} + +//MARK:- ViewModel related +extension ForecastCollectionView { + func initViewModel() { + forecastListViewModel.forecastList.listener = { forecastList in + DispatchQueue.main.async { + self.reloadData() + } + } + } +} + +//MARK:- UICollectionViewDataSource related +extension ForecastCollectionView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return forecastListViewModel.forecastCount + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "forecastCellIdentifier", for: indexPath) as! ForecastCell + let forecastViewModel = forecastListViewModel.getForecastViewModel(indexPath.item) + cell.configureCell(forecastViewModel: forecastViewModel, isNow: indexPath.item == 0) + return cell + } +} + +//MARK:- UICollectionViewDelegate related +extension ForecastCollectionView: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + forecastListViewModel.userSelectedForecast(indexPath.item) + } +} diff --git a/Weather/View/ForecastDailyCollectionView.swift b/Weather/View/ForecastDailyCollectionView.swift new file mode 100644 index 00000000..cfc5ce20 --- /dev/null +++ b/Weather/View/ForecastDailyCollectionView.swift @@ -0,0 +1,54 @@ +// +// ForecastDailyCollectionView.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastDailyCollectionView: UICollectionView { + + var forecastDailyListViewModel = ForecastDailyListViewModel() + + override func awakeFromNib() { + super.awakeFromNib() + self.register(UINib(nibName: "ForecastCell", bundle: nil), forCellWithReuseIdentifier: "forecastCellIdentifier") + self.dataSource = self + self.delegate = self + } + +} + +//MARK:- ViewModel related +extension ForecastDailyCollectionView { + func initViewModel() { + forecastDailyListViewModel.forecastDailyList.listener = { forecastList in + DispatchQueue.main.async { + self.reloadData() + } + } + } +} + +//MARK:- UICollectionViewDataSource related +extension ForecastDailyCollectionView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return forecastDailyListViewModel.forecastDailyCount + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "forecastCellIdentifier", for: indexPath) as! ForecastCell + let forecastDailyViewModel = forecastDailyListViewModel.getForecastDailyViewModel(index: indexPath.item) + cell.configureCell(forecastDailyViewModel: forecastDailyViewModel, isNow: indexPath.item == 0) + return cell + } +} + +//MARK:- UICollectionViewDelegate related +extension ForecastDailyCollectionView: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + forecastDailyListViewModel.userSelectedForecast(indexPath.item) + } +} diff --git a/Weather/View/ForecastDailyStackView.swift b/Weather/View/ForecastDailyStackView.swift new file mode 100644 index 00000000..891fa955 --- /dev/null +++ b/Weather/View/ForecastDailyStackView.swift @@ -0,0 +1,49 @@ +// +// ForecastDailyStackView.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastDailyStackView: UIStackView { + + var forecastDailyListViewModel = ForecastDailyListViewModel() + +} + +//MARK:- ViewModel related +extension ForecastDailyStackView { + func initViewModel() { + forecastDailyListViewModel.forecastDailyList.listener = { forecastDailyList in + DispatchQueue.main.async { + self.reloadData() + } + } + } +} + +extension ForecastDailyStackView { + private func reloadData() { + let count = forecastDailyListViewModel.forecastDailyCount + + if count > self.arrangedSubviews.count { + for _ in self.arrangedSubviews.count.. 0 && todayViewTopConstraint.constant > 0) || + (offset.y < 0 && todayViewTopConstraint.constant < todayViewTopDefault) { + let shift = offset.y + headerViewTopConstraint.constant -= shift * headerViewTopRatio + todayViewTopConstraint.constant -= shift * todayViewTopRatio + headerViewTopConstraint.constant = max(0, min(headerViewTopDefault, headerViewTopConstraint.constant)) + todayViewTopConstraint.constant = max(0, min(todayViewTopDefault, todayViewTopConstraint.constant)) + + self.view.layoutIfNeeded() + + let maxValue = (todayViewTopDefault / 2) + var alpha = (todayViewTopConstraint.constant - maxValue) / maxValue + alpha = max(0, alpha) + temperatureLabel.alpha = alpha + todayForecastView.alpha = alpha + + offset.y = 0 + scrollView.contentOffset = offset + } + } +} + diff --git a/Weather/ViewModel/ForecastDailyListViewModel.swift b/Weather/ViewModel/ForecastDailyListViewModel.swift new file mode 100644 index 00000000..55d4f22e --- /dev/null +++ b/Weather/ViewModel/ForecastDailyListViewModel.swift @@ -0,0 +1,60 @@ +// +// ForecastDailyListViewModel.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class ForecastDailyListViewModel: NSObject { + private var forecastDailyRawList = [ForecastDaily]() + let forecastDailyList: Dynamic<[ForecastDailyViewModel]> = Dynamic([]) + var forecastDailyCount: Int { + return forecastDailyList.value.count + } + var status: String? = nil + + var networkManager: WeatherNetworkManager! + var weatherDetail: WeatherDetailProtocol? +} + +extension ForecastDailyListViewModel { + func fetchForecastDaily(coordinate: Coordinate) { + networkManager.fetchForecastDaily(latitude: coordinate.latitude, longitude: coordinate.longitude) { (forecastDailyResponse, error) in + if let error = error { + print("Network Error: \(error)") + self.status = error + + DispatchQueue.main.async { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + self.fetchForecastDaily(coordinate: coordinate) + }) + } + } else { + self.status = nil + self.processForecastDailyResponse(forecastDailyResponse!) + } + } + } + + func getForecastDailyViewModel(index: Int) -> ForecastDailyViewModel { + return forecastDailyList.value[index] + } + + func userSelectedForecast(_ index: Int){ + weatherDetail?.updateWeatherData(forecastDaily: forecastDailyRawList[index]) + } +} + +extension ForecastDailyListViewModel { + private func processForecastDailyResponse(_ forecastDailyResponse: ForecastDailyResponse) { + forecastDailyRawList = forecastDailyResponse.list + forecastDailyList.value = forecastDailyResponse.list.map({ forecastDaily -> ForecastDailyViewModel in + return ForecastDailyViewModel(forecastDaily: forecastDaily) + }) + + } +} diff --git a/Weather/ViewModel/ForecastDailyViewModel.swift b/Weather/ViewModel/ForecastDailyViewModel.swift new file mode 100644 index 00000000..6d2cd0a4 --- /dev/null +++ b/Weather/ViewModel/ForecastDailyViewModel.swift @@ -0,0 +1,26 @@ +// +// ForecastDailyViewModel.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class ForecastDailyViewModel: NSObject { + let dayString: String + let dayShortString: String + var icon: String + let temperatureMax: Double + let temperatureMin: Double + + init(forecastDaily: ForecastDaily) { + dayString = DateConverter.timeIntervalToDayString(forecastDaily.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(forecastDaily.dayTime) + icon = "http://openweathermap.org/img/w/\(forecastDaily.weather[0].icon).png" + temperatureMax = forecastDaily.temperature.max + temperatureMin = forecastDaily.temperature.min + } +} diff --git a/Weather/ViewModel/ForecastListViewModel.swift b/Weather/ViewModel/ForecastListViewModel.swift new file mode 100644 index 00000000..313d838b --- /dev/null +++ b/Weather/ViewModel/ForecastListViewModel.swift @@ -0,0 +1,60 @@ +// +// ForecastListViewModel.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastListViewModel: NSObject { + private var forecastRawList = [Forecast]() + let forecastList: Dynamic<[ForecastViewModel]> = Dynamic([]) + var forecastCount: Int { + return forecastList.value.count + } + var status: String? = nil + + var networkManager: WeatherNetworkManager! + var weatherDetail: WeatherDetailProtocol? + +} + +extension ForecastListViewModel { + func fetchForecast(coordinate: Coordinate) { + networkManager.fetchForecast(latitude: coordinate.latitude, longitude: coordinate.longitude) { (forecastResponse, error) in + if let error = error { + print("Network Error: \(error)") + self.status = error + + DispatchQueue.main.async { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + self.fetchForecast(coordinate: coordinate) + }) + } + } else { + self.status = nil + self.processForecastResponse(forecastResponse!) + } + } + } + + func getForecastViewModel(_ index: Int) -> ForecastViewModel { + return forecastList.value[index] + } + + func userSelectedForecast(_ index: Int){ + weatherDetail?.updateWeatherData(forecast: forecastRawList[index]) + } +} + +extension ForecastListViewModel { + private func processForecastResponse(_ forecastResponse: ForecastResponse) { + forecastRawList = forecastResponse.list + forecastList.value = forecastResponse.list.map({ forecast -> ForecastViewModel in + return ForecastViewModel(forecast: forecast) + }) + + } +} diff --git a/Weather/ViewModel/ForecastViewModel.swift b/Weather/ViewModel/ForecastViewModel.swift new file mode 100644 index 00000000..8403a721 --- /dev/null +++ b/Weather/ViewModel/ForecastViewModel.swift @@ -0,0 +1,22 @@ +// +// ForecastViewModel.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class ForecastViewModel: NSObject { + var time: String + var temperature: String + var icon: String + + init(forecast: Forecast) { + time = DateConverter.timeIntervalToHourString(forecast.dayTime) + temperature = "\(Int(forecast.main.temperature))°" + icon = "http://openweathermap.org/img/w/\(forecast.weather[0].icon).png" + } +} diff --git a/Weather/ViewModel/WeatherDetailProtocol.swift b/Weather/ViewModel/WeatherDetailProtocol.swift new file mode 100644 index 00000000..2aeed106 --- /dev/null +++ b/Weather/ViewModel/WeatherDetailProtocol.swift @@ -0,0 +1,14 @@ +// +// WeatherDetailProtocol.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + +protocol WeatherDetailProtocol { + func updateWeatherData(forecast: Forecast) + func updateWeatherData(forecastDaily: ForecastDaily) +} diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift new file mode 100644 index 00000000..e951c73c --- /dev/null +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -0,0 +1,201 @@ +// +// WeatherViewModel.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit +import CoreLocation + +class WeatherViewModel: NSObject { + var location: Coordinate? + var area = "" + var country = "" + var weather = "" + var temperature: Double = 0 + var summary = "" + var dayTime: DayTime = .morning + var timeString = "00:00" + var dayString = "" + var dayShortString = "" + var temperatureMin: Double = 0 + var temperatureMax: Double = 0 + var sunrise = "00:00" + var sunriseTime: TimeInterval = 0 + var sunset = "00:00" + var sunsetTime: TimeInterval = 0 + var windSpeed = "" + var windDegree: Double? = 0 + var pressure = "" + var humidity = "" + var icon = "" + var status: String? = nil + + + var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) + private var locationManager = LocationManager() + + weak var forecastListViewModel: ForecastListViewModel? { + didSet { + forecastListViewModel?.networkManager = networkManager + forecastListViewModel?.weatherDetail = self + } + } + weak var forecastDailyListViewModel: ForecastDailyListViewModel? { + didSet { + forecastDailyListViewModel?.networkManager = networkManager + forecastDailyListViewModel?.weatherDetail = self + } + } + + var startFetchingWeatherClosure: (()->())? + var updateWeatherDataClosure: (()->())? + var finishedFetchingWeatherClosure: (()->())? + + + +} + +//MARK: - public func +extension WeatherViewModel { + func startWeatherForecasting() { + initLocationManager() + locationManager.requestLocation() + startFetchingWeatherClosure?() + } + + func fetchWeather() { + guard let location = location else { + print("Location is not avaliable") + status = "Location is not avaliable" + return + } + + networkManager.fetchWeather(latitude: location.latitude, longitude: location.longitude) { (weatherResponse, error) in + if let error = error { + print("Network Error: \(error)") + self.status = error + + DispatchQueue.main.async { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + self.fetchWeather() + }) + } + } else { + self.status = nil + self.processWeatherResponse(weatherResponse!) + } + } + } +} + +//MARK: - WeatherDetailProtocol +extension WeatherViewModel: WeatherDetailProtocol { + + func updateWeatherData(forecast: Forecast) { + + weather = forecast.weather[0].main + temperature = forecast.main.temperature + summary = "" + timeString = DateConverter.timeIntervalToHourMinuteString(forecast.dayTime) + dayString = DateConverter.timeIntervalToDayString(forecast.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(forecast.dayTime) + dayTime = self.calcDayTime(timeInterval: forecast.dayTime) + temperatureMin = forecast.main.temperatureMin + temperatureMax = forecast.main.temperatureMax + windSpeed = "\(forecast.wind.speed) mps" + windDegree = forecast.wind.degree + pressure = String(format: "%d hPa", Int(forecast.main.pressure)) + humidity = String(format: "%d %%", forecast.main.humidity) + icon = "http://openweathermap.org/img/w/\(forecast.weather[0].icon).png" + + updateWeatherDataClosure?() + } + + func updateWeatherData(forecastDaily: ForecastDaily) { + weather = forecastDaily.weather[0].main + temperature = forecastDaily.temperature.day + summary = "" + timeString = DateConverter.timeIntervalToHourMinuteString(forecastDaily.dayTime) + dayString = DateConverter.timeIntervalToDayString(forecastDaily.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(forecastDaily.dayTime) + dayTime = self.calcDayTime(timeInterval: forecastDaily.dayTime) + temperatureMin = forecastDaily.temperature.min + temperatureMax = forecastDaily.temperature.max + windSpeed = "\(forecastDaily.speed) mps" + windDegree = forecastDaily.degree + pressure = String(format: "%d hPa", Int(forecastDaily.pressure)) + humidity = String(format: "%d %%", forecastDaily.humidity) + icon = "http://openweathermap.org/img/w/\(forecastDaily.weather[0].icon).png" + + updateWeatherDataClosure?() + } +} + +//MARK: - private func +extension WeatherViewModel { + private func initLocationManager() { + if locationManager.locationUpdateClosure == nil { + locationManager.locationUpdateClosure = { location, placemark in + if let location = location { + print("Location: \(location.coordinate.latitude) - \(location.coordinate.longitude)") + self.location = Coordinate(longitude: location.coordinate.longitude, latitude: location.coordinate.latitude) + self.fetchWeather() + self.forecastListViewModel?.fetchForecast(coordinate: self.location!) + self.forecastDailyListViewModel?.fetchForecastDaily(coordinate: self.location!) + } else { + // cannot get user location + } + } + } + } + + private func processWeatherResponse(_ response: WeatherResponse) { + area = response.name + country = response.system.country + weather = response.weather[0].main + temperature = response.main.temperature + summary = String(format: "Today: %@ currently. It's %d°; The high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) + + timeString = DateConverter.timeIntervalToHourMinuteString(response.dayTime) + dayString = DateConverter.timeIntervalToDayString(response.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(response.dayTime) + temperatureMin = response.main.temperatureMin + temperatureMax = response.main.temperatureMax + sunrise = DateConverter.timeIntervalToHourMinuteString(response.system.sunrise) + sunriseTime = DateConverter.timeIntervalToDayTimeInterval(response.system.sunrise) + sunset = DateConverter.timeIntervalToHourMinuteString(response.system.sunset) + sunsetTime = DateConverter.timeIntervalToDayTimeInterval(response.system.sunset) + dayTime = self.calcDayTime(timeInterval: response.dayTime) + windSpeed = "\(response.wind.speed) mps" + windDegree = response.wind.degree + pressure = String(format: "%d hPa", Int(response.main.pressure)) + humidity = String(format: "%d %%", response.main.humidity) + icon = "http://openweathermap.org/img/w/\(response.weather[0].icon).png" + + updateWeatherDataClosure?() + finishedFetchingWeatherClosure?() + } + + private func calcDayTime(timeInterval: TimeInterval)-> DayTime { + let time = DateConverter.timeIntervalToDayTimeInterval(timeInterval) + var adjustment: TimeInterval = 7200 + if sunriseTime + adjustment > sunsetTime - adjustment { + adjustment = (sunsetTime - sunriseTime) / 4 + } + switch time { + case 0.. Data { + let bundle = Bundle(for: classForCoder) + let url = bundle.url(forResource: name, withExtension: `extension`) + + let data = try! Data(contentsOf: url!) + return data + } + + func loadJSONDictionaryFromBundle(withName name: String, extension: String) -> Dictionary { + let bundle = Bundle(for: classForCoder) + let url = bundle.url(forResource: name, withExtension: `extension`) + + let data = try! Data(contentsOf: url!) + let jsonResult = try! JSONSerialization.jsonObject(with: data, options: []) + let jsonDic = jsonResult as! Dictionary + return jsonDic + } + + func loadJSONArrayFromBundle(withName name: String, extension: String) -> [Dictionary] { + let bundle = Bundle(for: classForCoder) + let url = bundle.url(forResource: name, withExtension: `extension`) + + let data = try! Data(contentsOf: url!) + let jsonResult = try! JSONSerialization.jsonObject(with: data, options: []) + let jsonDicArray = jsonResult as! [Dictionary] + return jsonDicArray + } + + +} diff --git a/WeatherTests/ForecastListViewModelTests.swift b/WeatherTests/ForecastListViewModelTests.swift new file mode 100644 index 00000000..42ffea8a --- /dev/null +++ b/WeatherTests/ForecastListViewModelTests.swift @@ -0,0 +1,56 @@ +// +// ForecastListViewModelTests.swift +// WeatherTests +// +// Created by Christian Huang on 12/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import XCTest +@testable import Weather + +class ForecastListViewModelTests: XCTestCase { + var forecastListViewModel = ForecastListViewModel() + var networkManager = MockNetworkManager(apiKey: "testAPIKey", environment: .qa) + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + forecastListViewModel.networkManager = networkManager + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testFetchForecastError() { + networkManager.mockForecastResponse = nil + networkManager.mockError = "This is an error" + + let coordinate = Coordinate(longitude: 1, latitude: 2) + forecastListViewModel.fetchForecast(coordinate: coordinate) + + XCTAssertEqual(forecastListViewModel.status, networkManager.mockError) + } + + func testFetchForecast() { + let data = loadDataFromBundle(withName: "forecast", extension: "json") + let forecastResponse = try! JSONDecoder().decode(ForecastResponse.self, from: data) + + networkManager.mockForecastResponse = forecastResponse + networkManager.mockError = nil + + let coordinate = Coordinate(longitude: 1, latitude: 2) + forecastListViewModel.fetchForecast(coordinate: coordinate) + + XCTAssertNil(forecastListViewModel.status) + + XCTAssertEqual(forecastListViewModel.forecastList.value.count, 40) + + } + +} diff --git a/WeatherTests/Info.plist b/WeatherTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/WeatherTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/WeatherTests/MockService/MockNetworkManager.swift b/WeatherTests/MockService/MockNetworkManager.swift new file mode 100644 index 00000000..fa83c554 --- /dev/null +++ b/WeatherTests/MockService/MockNetworkManager.swift @@ -0,0 +1,36 @@ +// +// MockNetworkManager.swift +// WeatherTests +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation +@testable import Weather + +class MockNetworkManager: WeatherNetworkManager { + var apiKey: String + var mockWeatherResponse: WeatherResponse? + var mockForecastResponse: ForecastResponse? + var mockForecastDailyResponse: ForecastDailyResponse? + var mockError: String? + + required init(apiKey: String, environment: NetworkEnvironment) { + self.apiKey = apiKey + } + + func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (WeatherResponse?, String?) -> ()) { + completion(mockWeatherResponse, mockError) + } + + func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (ForecastResponse?, String?) -> ()) { + completion(mockForecastResponse, mockError) + } + + func fetchForecastDaily(latitude: Double, longitude: Double, completion: @escaping (ForecastDailyResponse?, String?) -> ()) { + completion(mockForecastDailyResponse, mockError) + } + + +} diff --git a/WeatherTests/Resource/forecast.json b/WeatherTests/Resource/forecast.json new file mode 100644 index 00000000..3776d253 --- /dev/null +++ b/WeatherTests/Resource/forecast.json @@ -0,0 +1 @@ +{"cod":"200","message":0.005,"cnt":40,"list":[{"dt":1541430000,"main":{"temp":27.54,"temp_min":27.54,"temp_max":27.62,"pressure":1022.93,"sea_level":1023.49,"grnd_level":1022.93,"humidity":99,"temp_kf":-0.08},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":32},"wind":{"speed":1.92,"deg":209.504},"rain":{"3h":1.4625},"sys":{"pod":"n"},"dt_txt":"2018-11-05 15:00:00"},{"dt":1541440800,"main":{"temp":27.12,"temp_min":27.12,"temp_max":27.18,"pressure":1021.52,"sea_level":1022.15,"grnd_level":1021.52,"humidity":100,"temp_kf":-0.06},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":2.37,"deg":197.504},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-05 18:00:00"},{"dt":1541451600,"main":{"temp":26.71,"temp_min":26.71,"temp_max":26.75,"pressure":1021.17,"sea_level":1021.83,"grnd_level":1021.17,"humidity":100,"temp_kf":-0.04},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":32},"wind":{"speed":2.64,"deg":186.001},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-05 21:00:00"},{"dt":1541462400,"main":{"temp":26.87,"temp_min":26.87,"temp_max":26.89,"pressure":1022.96,"sea_level":1023.48,"grnd_level":1022.96,"humidity":100,"temp_kf":-0.02},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":32},"wind":{"speed":3.19,"deg":155.5},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-06 00:00:00"},{"dt":1541473200,"main":{"temp":30.11,"temp_min":30.11,"temp_max":30.11,"pressure":1022.75,"sea_level":1023.31,"grnd_level":1022.75,"humidity":90,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"02d"}],"clouds":{"all":8},"wind":{"speed":1.95,"deg":108.516},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-06 03:00:00"},{"dt":1541484000,"main":{"temp":31.08,"temp_min":31.08,"temp_max":31.08,"pressure":1020.01,"sea_level":1020.64,"grnd_level":1020.01,"humidity":83,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":3.56,"deg":10.5087},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-06 06:00:00"},{"dt":1541494800,"main":{"temp":29.78,"temp_min":29.78,"temp_max":29.78,"pressure":1019.54,"sea_level":1020.1,"grnd_level":1019.54,"humidity":90,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":20},"wind":{"speed":2.17,"deg":17.5022},"rain":{"3h":3.17},"sys":{"pod":"d"},"dt_txt":"2018-11-06 09:00:00"},{"dt":1541505600,"main":{"temp":27.62,"temp_min":27.62,"temp_max":27.62,"pressure":1021.46,"sea_level":1022,"grnd_level":1021.46,"humidity":99,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":0},"wind":{"speed":1.26,"deg":176.501},"rain":{"3h":2.32},"sys":{"pod":"n"},"dt_txt":"2018-11-06 12:00:00"},{"dt":1541516400,"main":{"temp":27.35,"temp_min":27.35,"temp_max":27.35,"pressure":1022.87,"sea_level":1023.39,"grnd_level":1022.87,"humidity":99,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":24},"wind":{"speed":1.89,"deg":158.502},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-06 15:00:00"},{"dt":1541527200,"main":{"temp":26.79,"temp_min":26.79,"temp_max":26.79,"pressure":1021.5,"sea_level":1022.14,"grnd_level":1021.5,"humidity":100,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":1.76,"deg":170},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-06 18:00:00"},{"dt":1541538000,"main":{"temp":26.63,"temp_min":26.63,"temp_max":26.63,"pressure":1020.93,"sea_level":1021.53,"grnd_level":1020.93,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":64},"wind":{"speed":1.72,"deg":164.003},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-06 21:00:00"},{"dt":1541548800,"main":{"temp":27,"temp_min":27,"temp_max":27,"pressure":1022.2,"sea_level":1022.75,"grnd_level":1022.2,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":48},"wind":{"speed":1.06,"deg":91.501},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-07 00:00:00"},{"dt":1541559600,"main":{"temp":29.07,"temp_min":29.07,"temp_max":29.07,"pressure":1022.54,"sea_level":1023.01,"grnd_level":1022.54,"humidity":97,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":12},"wind":{"speed":1.72,"deg":18.5053},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-07 03:00:00"},{"dt":1541570400,"main":{"temp":29.7,"temp_min":29.7,"temp_max":29.7,"pressure":1020.29,"sea_level":1020.93,"grnd_level":1020.29,"humidity":92,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":0},"wind":{"speed":4.61,"deg":348.007},"rain":{"3h":0.46},"sys":{"pod":"d"},"dt_txt":"2018-11-07 06:00:00"},{"dt":1541581200,"main":{"temp":29.59,"temp_min":29.59,"temp_max":29.59,"pressure":1019.9,"sea_level":1020.47,"grnd_level":1019.9,"humidity":90,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":8},"wind":{"speed":2.77,"deg":321.501},"rain":{"3h":1.805},"sys":{"pod":"d"},"dt_txt":"2018-11-07 09:00:00"},{"dt":1541592000,"main":{"temp":28.22,"temp_min":28.22,"temp_max":28.22,"pressure":1021.63,"sea_level":1022.19,"grnd_level":1021.63,"humidity":93,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":36},"wind":{"speed":1.52,"deg":247.002},"rain":{"3h":1.235},"sys":{"pod":"n"},"dt_txt":"2018-11-07 12:00:00"},{"dt":1541602800,"main":{"temp":27.31,"temp_min":27.31,"temp_max":27.31,"pressure":1023.05,"sea_level":1023.54,"grnd_level":1023.05,"humidity":99,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":1.66,"deg":209.503},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-07 15:00:00"},{"dt":1541613600,"main":{"temp":26.88,"temp_min":26.88,"temp_max":26.88,"pressure":1021.78,"sea_level":1022.41,"grnd_level":1021.78,"humidity":100,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":20},"wind":{"speed":1.86,"deg":203.5},"rain":{"3h":0.004999999999999},"sys":{"pod":"n"},"dt_txt":"2018-11-07 18:00:00"},{"dt":1541624400,"main":{"temp":26.42,"temp_min":26.42,"temp_max":26.42,"pressure":1021.13,"sea_level":1021.63,"grnd_level":1021.13,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":32},"wind":{"speed":1.95,"deg":198.502},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-07 21:00:00"},{"dt":1541635200,"main":{"temp":27.01,"temp_min":27.01,"temp_max":27.01,"pressure":1022.36,"sea_level":1022.88,"grnd_level":1022.36,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":36},"wind":{"speed":2.07,"deg":171.001},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-08 00:00:00"},{"dt":1541646000,"main":{"temp":29.29,"temp_min":29.29,"temp_max":29.29,"pressure":1022.63,"sea_level":1023.13,"grnd_level":1022.63,"humidity":94,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":68},"wind":{"speed":0.55,"deg":99.5026},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-08 03:00:00"},{"dt":1541656800,"main":{"temp":30.08,"temp_min":30.08,"temp_max":30.08,"pressure":1020.64,"sea_level":1021.17,"grnd_level":1020.64,"humidity":88,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":64},"wind":{"speed":3.01,"deg":13.5023},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-08 06:00:00"},{"dt":1541667600,"main":{"temp":29.73,"temp_min":29.73,"temp_max":29.73,"pressure":1019.76,"sea_level":1020.38,"grnd_level":1019.76,"humidity":89,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":36},"wind":{"speed":2.21,"deg":1.0033},"rain":{"3h":0.91},"sys":{"pod":"d"},"dt_txt":"2018-11-08 09:00:00"},{"dt":1541678400,"main":{"temp":26.8,"temp_min":26.8,"temp_max":26.8,"pressure":1021.49,"sea_level":1021.94,"grnd_level":1021.49,"humidity":100,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10n"}],"clouds":{"all":56},"wind":{"speed":1.61,"deg":119.502},"rain":{"3h":3.5625},"sys":{"pod":"n"},"dt_txt":"2018-11-08 12:00:00"},{"dt":1541689200,"main":{"temp":26.86,"temp_min":26.86,"temp_max":26.86,"pressure":1022.58,"sea_level":1023.19,"grnd_level":1022.58,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":76},"wind":{"speed":1.06,"deg":149.002},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-08 15:00:00"},{"dt":1541700000,"main":{"temp":27.04,"temp_min":27.04,"temp_max":27.04,"pressure":1021.53,"sea_level":1022.03,"grnd_level":1021.53,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":68},"wind":{"speed":1.31,"deg":152.505},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-08 18:00:00"},{"dt":1541710800,"main":{"temp":26.79,"temp_min":26.79,"temp_max":26.79,"pressure":1020.43,"sea_level":1020.91,"grnd_level":1020.43,"humidity":100,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":2.27,"deg":122.003},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-08 21:00:00"},{"dt":1541721600,"main":{"temp":27.68,"temp_min":27.68,"temp_max":27.68,"pressure":1021.55,"sea_level":1022.11,"grnd_level":1021.55,"humidity":100,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":24},"wind":{"speed":2.66,"deg":120.006},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-09 00:00:00"},{"dt":1541732400,"main":{"temp":29.82,"temp_min":29.82,"temp_max":29.82,"pressure":1021.95,"sea_level":1022.39,"grnd_level":1021.95,"humidity":93,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":68},"wind":{"speed":2.06,"deg":87.0087},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-09 03:00:00"},{"dt":1541743200,"main":{"temp":30.48,"temp_min":30.48,"temp_max":30.48,"pressure":1019.51,"sea_level":1020.11,"grnd_level":1019.51,"humidity":88,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":48},"wind":{"speed":3.36,"deg":35.0016},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-09 06:00:00"},{"dt":1541754000,"main":{"temp":30.05,"temp_min":30.05,"temp_max":30.05,"pressure":1018.86,"sea_level":1019.55,"grnd_level":1018.86,"humidity":90,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":24},"wind":{"speed":2.48,"deg":56.0006},"rain":{"3h":2.8875},"sys":{"pod":"d"},"dt_txt":"2018-11-09 09:00:00"},{"dt":1541764800,"main":{"temp":26.49,"temp_min":26.49,"temp_max":26.49,"pressure":1021.02,"sea_level":1021.44,"grnd_level":1021.02,"humidity":100,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10n"}],"clouds":{"all":44},"wind":{"speed":2.97,"deg":151.501},"rain":{"3h":8.2375},"sys":{"pod":"n"},"dt_txt":"2018-11-09 12:00:00"},{"dt":1541775600,"main":{"temp":27.01,"temp_min":27.01,"temp_max":27.01,"pressure":1022.4,"sea_level":1022.96,"grnd_level":1022.4,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":48},"wind":{"speed":2.02,"deg":121.5},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-09 15:00:00"},{"dt":1541786400,"main":{"temp":26.82,"temp_min":26.82,"temp_max":26.82,"pressure":1021.15,"sea_level":1021.7,"grnd_level":1021.15,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":36},"wind":{"speed":2.17,"deg":128.5},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-09 18:00:00"},{"dt":1541797200,"main":{"temp":26.44,"temp_min":26.44,"temp_max":26.44,"pressure":1020.56,"sea_level":1021.16,"grnd_level":1020.56,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":32},"wind":{"speed":2.43,"deg":140.005},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-09 21:00:00"},{"dt":1541808000,"main":{"temp":26.86,"temp_min":26.86,"temp_max":26.86,"pressure":1022.34,"sea_level":1022.87,"grnd_level":1022.34,"humidity":100,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":88},"wind":{"speed":2.6,"deg":116.014},"rain":{"3h":0.037500000000001},"sys":{"pod":"d"},"dt_txt":"2018-11-10 00:00:00"},{"dt":1541818800,"main":{"temp":28.66,"temp_min":28.66,"temp_max":28.66,"pressure":1022.61,"sea_level":1023.19,"grnd_level":1022.61,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":56},"wind":{"speed":2.11,"deg":91.5005},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-10 03:00:00"},{"dt":1541829600,"main":{"temp":29.62,"temp_min":29.62,"temp_max":29.62,"pressure":1020.59,"sea_level":1021.09,"grnd_level":1020.59,"humidity":96,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":32},"wind":{"speed":3.83,"deg":41.0005},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-10 06:00:00"},{"dt":1541840400,"main":{"temp":27.24,"temp_min":27.24,"temp_max":27.24,"pressure":1020.09,"sea_level":1020.41,"grnd_level":1020.09,"humidity":100,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":44},"wind":{"speed":2.56,"deg":93.0006},"rain":{"3h":6.575},"sys":{"pod":"d"},"dt_txt":"2018-11-10 09:00:00"},{"dt":1541851200,"main":{"temp":27.01,"temp_min":27.01,"temp_max":27.01,"pressure":1021.26,"sea_level":1021.85,"grnd_level":1021.26,"humidity":100,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":56},"wind":{"speed":1.76,"deg":148.006},"rain":{"3h":0.049999999999997},"sys":{"pod":"n"},"dt_txt":"2018-11-10 12:00:00"}],"city":{"id":1993378,"name":"Cideng","coord":{"lat":-6.1674,"lon":106.8062},"country":"ID"}} diff --git a/WeatherTests/Resource/forecastDaily.json b/WeatherTests/Resource/forecastDaily.json new file mode 100644 index 00000000..3dca4940 --- /dev/null +++ b/WeatherTests/Resource/forecastDaily.json @@ -0,0 +1 @@ +{"city":{"id":1993378,"name":"Cideng","coord":{"lon":106.8062,"lat":-6.1674},"country":"ID","population":0},"cod":"200","message":10.2448179,"cnt":7,"list":[{"dt":1541390400,"temp":{"day":29,"min":27.54,"max":29,"night":27.54,"eve":29,"morn":29},"pressure":1021.44,"humidity":88,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":1.06,"deg":224,"clouds":64,"rain":2.48},{"dt":1541476800,"temp":{"day":30.11,"min":26.71,"max":31.08,"night":27.35,"eve":29.78,"morn":26.71},"pressure":1022.75,"humidity":90,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":1.95,"deg":109,"clouds":8,"rain":5.49},{"dt":1541563200,"temp":{"day":29.07,"min":26.63,"max":29.7,"night":27.31,"eve":29.59,"morn":26.63},"pressure":1022.54,"humidity":97,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":1.72,"deg":19,"clouds":12,"rain":3.5},{"dt":1541649600,"temp":{"day":29.29,"min":26.42,"max":30.08,"night":26.86,"eve":29.73,"morn":26.42},"pressure":1022.63,"humidity":94,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":0.55,"deg":100,"clouds":68,"rain":4.47},{"dt":1541736000,"temp":{"day":28.61,"min":26.42,"max":28.61,"night":26.42,"eve":27.37,"morn":26.9},"pressure":1015.86,"humidity":0,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":3.95,"deg":58,"clouds":16,"rain":3.09},{"dt":1541822400,"temp":{"day":29.6,"min":26.43,"max":29.6,"night":26.43,"eve":27.71,"morn":26.84},"pressure":1015.36,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":3.12,"deg":53,"clouds":17,"rain":2.12},{"dt":1541908800,"temp":{"day":29.47,"min":26.16,"max":29.47,"night":26.16,"eve":27.76,"morn":27.13},"pressure":1015.6,"humidity":0,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":3.05,"deg":34,"clouds":57,"rain":7.01}]} diff --git a/WeatherTests/Resource/weather.json b/WeatherTests/Resource/weather.json new file mode 100644 index 00000000..96b2411a --- /dev/null +++ b/WeatherTests/Resource/weather.json @@ -0,0 +1 @@ +{"coord":{"lon":106.79,"lat":-6.18},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"base":"stations","main":{"temp":30.22,"pressure":1008,"humidity":78,"temp_min":28,"temp_max":32},"visibility":4000,"wind":{"speed":4.1,"deg":320},"clouds":{"all":40},"dt":1541916000,"sys":{"type":1,"id":8038,"message":0.0036,"country":"ID","sunrise":1541888729,"sunset":1541933292},"id":1993378,"name":"Cideng","cod":200} diff --git a/WeatherTests/WeatherTests.swift b/WeatherTests/WeatherTests.swift new file mode 100644 index 00000000..40928eea --- /dev/null +++ b/WeatherTests/WeatherTests.swift @@ -0,0 +1,27 @@ +// +// WeatherTests.swift +// WeatherTests +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import XCTest +@testable import Weather + +class WeatherTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + +} diff --git a/WeatherTests/WeatherViewModelTests.swift b/WeatherTests/WeatherViewModelTests.swift new file mode 100644 index 00000000..fc6f741f --- /dev/null +++ b/WeatherTests/WeatherViewModelTests.swift @@ -0,0 +1,120 @@ +// +// WeatherViewModelTests.swift +// WeatherTests +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import XCTest +@testable import Weather + +class WeatherViewModelTests: XCTestCase { + var weatherViewModel = WeatherViewModel() + var networkManager = MockNetworkManager(apiKey: "testAPIKey", environment: .qa) + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + weatherViewModel.networkManager = networkManager + weatherViewModel.location = Coordinate(longitude: 1, latitude: 2) + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testFetchWeatherError() { + networkManager.mockWeatherResponse = nil + networkManager.mockError = "This is an error" + + weatherViewModel.fetchWeather() + + XCTAssertEqual(weatherViewModel.status, networkManager.mockError) + } + + func testFetchWeather() { + let data = loadDataFromBundle(withName: "weather", extension: "json") + let weatherResponse = try! JSONDecoder().decode(WeatherResponse.self, from: data) + + networkManager.mockWeatherResponse = weatherResponse + networkManager.mockError = nil + + weatherViewModel.fetchWeather() + + XCTAssertNil(weatherViewModel.status) + + XCTAssertEqual(weatherViewModel.area, "Cideng") + XCTAssertEqual(weatherViewModel.weather, "Rain") + XCTAssertEqual(weatherViewModel.temperature, 30.22) + XCTAssertEqual(weatherViewModel.summary, "Today: Rain currently. It's 30°; The high today was forecast as 32°.") + XCTAssertEqual(weatherViewModel.timeString, "13:00") + XCTAssertEqual(weatherViewModel.dayString, "Sunday") + XCTAssertEqual(weatherViewModel.dayShortString, "Sun") + XCTAssertEqual(weatherViewModel.temperatureMin, 28) + XCTAssertEqual(weatherViewModel.temperatureMax, 32) + XCTAssertEqual(weatherViewModel.sunrise, "05:25") + XCTAssertEqual(weatherViewModel.sunriseTime, 19500) + XCTAssertEqual(weatherViewModel.sunset, "17:48") + XCTAssertEqual(weatherViewModel.sunsetTime, 64080) + XCTAssertEqual(weatherViewModel.dayTime, .day) + XCTAssertEqual(weatherViewModel.windSpeed, "4.1 mps") + XCTAssertEqual(weatherViewModel.pressure, "1008 hPa") + XCTAssertEqual(weatherViewModel.humidity, "78 %") + XCTAssertEqual(weatherViewModel.icon, "http://openweathermap.org/img/w/10d.png") + + } + + func testUpdateWeatherWithForecast() { + let data = loadDataFromBundle(withName: "forecast", extension: "json") + let forecastResponse = try! JSONDecoder().decode(ForecastResponse.self, from: data) + + weatherViewModel.sunriseTime = 19500 + weatherViewModel.sunsetTime = 64080 + + weatherViewModel.updateWeatherData(forecast: forecastResponse.list[2]) + + XCTAssertEqual(weatherViewModel.weather, "Clouds") + XCTAssertEqual(weatherViewModel.temperature, 26.71) + XCTAssertEqual(weatherViewModel.timeString, "04:00") + XCTAssertEqual(weatherViewModel.dayString, "Tuesday") + XCTAssertEqual(weatherViewModel.dayShortString, "Tue") + XCTAssertEqual(weatherViewModel.dayTime, .night) + XCTAssertEqual(weatherViewModel.temperatureMin, 26.71) + XCTAssertEqual(weatherViewModel.temperatureMax, 26.75) + XCTAssertEqual(weatherViewModel.windSpeed, "2.64 mps") + XCTAssertEqual(weatherViewModel.pressure, "1021 hPa") + XCTAssertEqual(weatherViewModel.humidity, "100 %") + XCTAssertEqual(weatherViewModel.icon, "http://openweathermap.org/img/w/03n.png") + } + + func testUpdateWeatherWithForecastDaily() { + let data = loadDataFromBundle(withName: "forecastDaily", extension: "json") + let forecastDailyResponse = try! JSONDecoder().decode(ForecastDailyResponse.self, from: data) + + weatherViewModel.sunriseTime = 19500 + weatherViewModel.sunsetTime = 64080 + + weatherViewModel.updateWeatherData(forecastDaily: forecastDailyResponse.list[3]) + + XCTAssertEqual(weatherViewModel.weather, "Rain") + XCTAssertEqual(weatherViewModel.temperature, 29.29) + XCTAssertEqual(weatherViewModel.timeString, "11:00") + XCTAssertEqual(weatherViewModel.dayString, "Thursday") + XCTAssertEqual(weatherViewModel.dayShortString, "Thu") + XCTAssertEqual(weatherViewModel.dayTime, .day) + XCTAssertEqual(weatherViewModel.temperatureMin, 26.42) + XCTAssertEqual(weatherViewModel.temperatureMax, 30.08) + XCTAssertEqual(weatherViewModel.windSpeed, "0.55 mps") + XCTAssertEqual(weatherViewModel.pressure, "1022 hPa") + XCTAssertEqual(weatherViewModel.humidity, "94 %") + XCTAssertEqual(weatherViewModel.icon, "http://openweathermap.org/img/w/10d.png") + } + + + +}