├── .gitignore
├── LICENSE.md
├── README.md
├── Weather.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ └── Weather.xcscheme
├── Weather
├── Resources
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Mocks
│ │ ├── MockOneCallResponse.json
│ │ ├── MockWeatherFetcher.swift
│ │ └── MockWeatherSummary.swift
│ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Sources
│ ├── DataManager
│ │ └── DataManager.swift
│ ├── Models
│ │ ├── OneCallResponse.swift
│ │ ├── WeatherError.swift
│ │ └── WeatherSummary.swift
│ ├── Utilities
│ │ └── DismissKeyboard.swift
│ └── Views
│ │ ├── CurrentSummary
│ │ ├── CurrentSummaryView.swift
│ │ └── CurrentSummaryViewModel.swift
│ │ ├── DaySummary
│ │ ├── DaySummaryView.swift
│ │ └── DaySummaryViewModel.swift
│ │ ├── HourSummary
│ │ ├── HourInformationViewModel.swift
│ │ ├── HourSummaryView.swift
│ │ └── HourSummaryViewModel.swift
│ │ ├── SearchBar
│ │ └── SearchBar.swift
│ │ └── WeatherSummary
│ │ ├── WeatherSummaryView.swift
│ │ └── WeatherSummaryViewModel.swift
└── Support
│ ├── AppDelegate.swift
│ ├── Info.plist
│ └── SceneDelegate.swift
├── WeatherTests
├── CurrentSummaryViewModelTests.swift
├── DaySummaryViewModelTests.swift
├── HourSummaryViewModelTests.swift
├── Info.plist
├── WeatherSummaryViewModelTests.swift
└── WeatherTests.swift
└── screenshots
├── screencap1.gif
└── screenshot1.png
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/xcode,macos,cocoapods
3 | # Edit at https://www.gitignore.io/?templates=xcode,macos,cocoapods
4 |
5 | ### CocoaPods ###
6 | ## CocoaPods GitIgnore Template
7 |
8 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing
9 | # - Also handy if you have a large number of dependant pods
10 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
11 | Pods/
12 |
13 | ### macOS ###
14 | # General
15 | .DS_Store
16 | .AppleDouble
17 | .LSOverride
18 |
19 | # Icon must end with two \r
20 | Icon
21 |
22 | # Thumbnails
23 | ._*
24 |
25 | # Files that might appear in the root of a volume
26 | .DocumentRevisions-V100
27 | .fseventsd
28 | .Spotlight-V100
29 | .TemporaryItems
30 | .Trashes
31 | .VolumeIcon.icns
32 | .com.apple.timemachine.donotpresent
33 |
34 | # Directories potentially created on remote AFP share
35 | .AppleDB
36 | .AppleDesktop
37 | Network Trash Folder
38 | Temporary Items
39 | .apdisk
40 |
41 | ### Xcode ###
42 | # Xcode
43 | #
44 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
45 |
46 | ## User settings
47 | xcuserdata/
48 |
49 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
50 | *.xcscmblueprint
51 | *.xccheckout
52 |
53 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
54 | build/
55 | DerivedData/
56 | *.moved-aside
57 | *.pbxuser
58 | !default.pbxuser
59 | *.mode1v3
60 | !default.mode1v3
61 | *.mode2v3
62 | !default.mode2v3
63 | *.perspectivev3
64 | !default.perspectivev3
65 |
66 | ## Xcode Patch
67 | *.xcodeproj/*
68 | !*.xcodeproj/project.pbxproj
69 | !*.xcodeproj/xcshareddata/
70 | !*.xcworkspace/contents.xcworkspacedata
71 | /*.gcno
72 |
73 | ### Xcode Patch ###
74 | **/xcshareddata/WorkspaceSettings.xcsettings
75 |
76 | # End of https://www.gitignore.io/api/xcode,macos,cocoapods
77 |
78 | Secrets.*
79 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Alex Fargo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI Weather
2 |
3 | This is an app I made in a weekend to learn more about using Combine and SwiftUI. I am pretty unfamiliar with Combine and haven't really used many reactive libraries before, so it was a little tough wrapping my head around how Combine works.
4 |
5 | ## Motivation
6 |
7 | I've been messing around with SwiftUI ever since it was announced at WWDC19 but not much with Combine. I was kind of afraid of it because of the few times I tried using it, I could never really understand exactly what I was doing.
8 |
9 | Now that I'm a bit more of a mature iOS developer (since I have been working professionally for a year in addition to the couple of years of unprofessional experience before that) I thought I would give Combine a shot again.
10 |
11 | ## Screenshots
12 |
13 |
14 |
15 | ## What I Learned
16 |
17 | So the main goal of this project was to learn more about using Combine and SwiftUI together. Boy did I certainly learn a few things...
18 |
19 | One of the biggest problems I ran into was searching for weather by named location.
20 |
21 | See, the endpoint I chose to use in the Open Weather API has two main parameters `lat` latitude and `lon` longitude. Now, obviously it would be a horrible UX if I had the users type in their coordinates just to search for weather conditions at a location, so I did some searching for a way to convert a location string to some coordinates. Luckily, Apple provides such a feature in the CoreLocation API. Great 🙌. Except it is an asynchronous call (makes sense) that does not use Combine 😰. I already had code up and running for making a call to the Open Weather API endpoint with a `dataTaskPublisher` and some predetermined coordinates which wasn't too bad to setup. But now I had to figure how to link together these two calls.
22 |
23 | Had I not been using Combine, I probably would have just used a Semaphore and used the result of the CoreLocation call in the Open Weather call. But no, we are using Combine!
24 |
25 | So, I learned about this other Publisher type called a `Future` which is a little more familiar looking cause it is an asynchronous call with a completion (kind of). With a `Future`, you have a `Promise`, which in this case is a closure to be called in the *future* with a `Result` type parameter. This was super easy to understand and implement.
26 |
27 | The hard part came when I had to link these two calls together. I knew I wanted the location call to occur first then the weather call, but how the heck do I do this in Combine?? I started doing lots of searching and basically discovered that `map` is my best friend. As long as I got the types to match at the beginning and end of each of the transformation methods, then everything should work out. Boy was it a pain figuring out the types on each end and how to get them to that point.
28 |
29 | You will see in the code that there are lots of `.eraseToAnyPublisher()` calls. At a basic level, what this does is what it sounds like, it strips the publisher type from the publisher to its bare bones, the `Output` type and the `Failure` type, then wraps it in `AnyPublisher`. Then you have some `Publisher` that has X output type and Y failure type. As long as you match those in your next publisher transformation, then you are good 👍
30 |
31 | Eventuall I got it to work and I was so relieved and happy. I really do feel like I understand Publishers and subscribers A LOT more now. Hopefully this all made sense... Feel free to email me if you want help or don't understand something I've said. Combine is super tricky and it won't make sense on your first try, but that's okay. Just keep trying and eventually it will click!
32 |
33 | ## Usage
34 |
35 | To download and use this project, you must obtain an Open Weather API key from [here](https://openweathermap.org/api).
36 |
37 | Once you have a key, navigate to the Support folder in the project and create a property list file named "Secrets.plist."
38 |
39 | Create a key named `openWeatherAPIKey` and assign the key that you obtained to the value.
40 |
41 | ## Contribute
42 |
43 | If you would like to contribute, please do!
44 |
45 | ## License
46 |
47 | MIT (c) [Alex Fargo](https://github.com/flexaargo) [(x)](https://twitter.com/flexaargo)
48 |
49 |
--------------------------------------------------------------------------------
/Weather.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | D07EF08B2456A0A4004189AC /* OneCallResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF08A2456A0A4004189AC /* OneCallResponse.swift */; };
11 | D07EF0922456A2CA004189AC /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0912456A2CA004189AC /* DataManager.swift */; };
12 | D07EF0942456A383004189AC /* WeatherSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ABD7492456993000840F37 /* WeatherSummaryView.swift */; };
13 | D07EF0972456A6EE004189AC /* MockOneCallResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = D07EF0962456A6EE004189AC /* MockOneCallResponse.json */; };
14 | D07EF09924575F3F004189AC /* WeatherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF09824575F3F004189AC /* WeatherError.swift */; };
15 | D07EF09B24575FCB004189AC /* Secrets.plist in Resources */ = {isa = PBXBuildFile; fileRef = D07EF09A24575FCB004189AC /* Secrets.plist */; };
16 | D07EF09D24576267004189AC /* WeatherSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF09C24576267004189AC /* WeatherSummary.swift */; };
17 | D07EF09E24578D6B004189AC /* WeatherSummaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF08C2456A26B004189AC /* WeatherSummaryViewModel.swift */; };
18 | D07EF0A024578E00004189AC /* MockWeatherFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF09F24578E00004189AC /* MockWeatherFetcher.swift */; };
19 | D07EF0A42457C196004189AC /* CurrentSummaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0A32457C196004189AC /* CurrentSummaryViewModel.swift */; };
20 | D07EF0A62457C1A7004189AC /* CurrentSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0A52457C1A7004189AC /* CurrentSummaryView.swift */; };
21 | D07EF0A92457C2E9004189AC /* MockWeatherSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0A82457C2E9004189AC /* MockWeatherSummary.swift */; };
22 | D07EF0AC2457D83F004189AC /* HourSummaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0AB2457D83F004189AC /* HourSummaryViewModel.swift */; };
23 | D07EF0AE2457D897004189AC /* HourSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0AD2457D897004189AC /* HourSummaryView.swift */; };
24 | D07EF0B12457E13B004189AC /* DaySummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0B02457E13B004189AC /* DaySummaryView.swift */; };
25 | D07EF0B32457E152004189AC /* DaySummaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0B22457E152004189AC /* DaySummaryViewModel.swift */; };
26 | D07EF0B62457ED40004189AC /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0B52457ED40004189AC /* SearchBar.swift */; };
27 | D07EF0B82457F203004189AC /* DismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0B72457F203004189AC /* DismissKeyboard.swift */; };
28 | D07EF0BA2457F5AD004189AC /* DaySummaryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0B92457F5AD004189AC /* DaySummaryViewModelTests.swift */; };
29 | D07EF0BE2457F742004189AC /* HourSummaryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0BD2457F742004189AC /* HourSummaryViewModelTests.swift */; };
30 | D07EF0C02457F869004189AC /* CurrentSummaryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0BF2457F869004189AC /* CurrentSummaryViewModelTests.swift */; };
31 | D07EF0C22457FA12004189AC /* WeatherSummaryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EF0C12457FA12004189AC /* WeatherSummaryViewModelTests.swift */; };
32 | D0ABD7462456993000840F37 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ABD7452456993000840F37 /* AppDelegate.swift */; };
33 | D0ABD7482456993000840F37 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ABD7472456993000840F37 /* SceneDelegate.swift */; };
34 | D0ABD74C2456993200840F37 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0ABD74B2456993200840F37 /* Assets.xcassets */; };
35 | D0ABD74F2456993200840F37 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0ABD74E2456993200840F37 /* Preview Assets.xcassets */; };
36 | D0ABD7522456993200840F37 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D0ABD7502456993200840F37 /* LaunchScreen.storyboard */; };
37 | D0ABD75D2456993200840F37 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ABD75C2456993200840F37 /* WeatherTests.swift */; };
38 | /* End PBXBuildFile section */
39 |
40 | /* Begin PBXContainerItemProxy section */
41 | D0ABD7592456993200840F37 /* PBXContainerItemProxy */ = {
42 | isa = PBXContainerItemProxy;
43 | containerPortal = D0ABD73A2456993000840F37 /* Project object */;
44 | proxyType = 1;
45 | remoteGlobalIDString = D0ABD7412456993000840F37;
46 | remoteInfo = Weather;
47 | };
48 | /* End PBXContainerItemProxy section */
49 |
50 | /* Begin PBXFileReference section */
51 | D07EF08A2456A0A4004189AC /* OneCallResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneCallResponse.swift; sourceTree = ""; };
52 | D07EF08C2456A26B004189AC /* WeatherSummaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSummaryViewModel.swift; sourceTree = ""; };
53 | D07EF0912456A2CA004189AC /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; };
54 | D07EF0962456A6EE004189AC /* MockOneCallResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = MockOneCallResponse.json; sourceTree = ""; };
55 | D07EF09824575F3F004189AC /* WeatherError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherError.swift; sourceTree = ""; };
56 | D07EF09A24575FCB004189AC /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Secrets.plist; sourceTree = ""; };
57 | D07EF09C24576267004189AC /* WeatherSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSummary.swift; sourceTree = ""; };
58 | D07EF09F24578E00004189AC /* MockWeatherFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWeatherFetcher.swift; sourceTree = ""; };
59 | D07EF0A32457C196004189AC /* CurrentSummaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentSummaryViewModel.swift; sourceTree = ""; };
60 | D07EF0A52457C1A7004189AC /* CurrentSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentSummaryView.swift; sourceTree = ""; };
61 | D07EF0A82457C2E9004189AC /* MockWeatherSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWeatherSummary.swift; sourceTree = ""; };
62 | D07EF0AB2457D83F004189AC /* HourSummaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourSummaryViewModel.swift; sourceTree = ""; };
63 | D07EF0AD2457D897004189AC /* HourSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourSummaryView.swift; sourceTree = ""; };
64 | D07EF0B02457E13B004189AC /* DaySummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaySummaryView.swift; sourceTree = ""; };
65 | D07EF0B22457E152004189AC /* DaySummaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaySummaryViewModel.swift; sourceTree = ""; };
66 | D07EF0B52457ED40004189AC /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; };
67 | D07EF0B72457F203004189AC /* DismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissKeyboard.swift; sourceTree = ""; };
68 | D07EF0B92457F5AD004189AC /* DaySummaryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaySummaryViewModelTests.swift; sourceTree = ""; };
69 | D07EF0BD2457F742004189AC /* HourSummaryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourSummaryViewModelTests.swift; sourceTree = ""; };
70 | D07EF0BF2457F869004189AC /* CurrentSummaryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentSummaryViewModelTests.swift; sourceTree = ""; };
71 | D07EF0C12457FA12004189AC /* WeatherSummaryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSummaryViewModelTests.swift; sourceTree = ""; };
72 | D0ABD7422456993000840F37 /* Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Weather.app; sourceTree = BUILT_PRODUCTS_DIR; };
73 | D0ABD7452456993000840F37 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
74 | D0ABD7472456993000840F37 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
75 | D0ABD7492456993000840F37 /* WeatherSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSummaryView.swift; sourceTree = ""; };
76 | D0ABD74B2456993200840F37 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
77 | D0ABD74E2456993200840F37 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
78 | D0ABD7512456993200840F37 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
79 | D0ABD7532456993200840F37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
80 | D0ABD7582456993200840F37 /* WeatherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
81 | D0ABD75C2456993200840F37 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; };
82 | D0ABD75E2456993200840F37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
83 | /* End PBXFileReference section */
84 |
85 | /* Begin PBXFrameworksBuildPhase section */
86 | D0ABD73F2456993000840F37 /* Frameworks */ = {
87 | isa = PBXFrameworksBuildPhase;
88 | buildActionMask = 2147483647;
89 | files = (
90 | );
91 | runOnlyForDeploymentPostprocessing = 0;
92 | };
93 | D0ABD7552456993200840F37 /* Frameworks */ = {
94 | isa = PBXFrameworksBuildPhase;
95 | buildActionMask = 2147483647;
96 | files = (
97 | );
98 | runOnlyForDeploymentPostprocessing = 0;
99 | };
100 | /* End PBXFrameworksBuildPhase section */
101 |
102 | /* Begin PBXGroup section */
103 | D07EF0892456A095004189AC /* Models */ = {
104 | isa = PBXGroup;
105 | children = (
106 | D07EF08A2456A0A4004189AC /* OneCallResponse.swift */,
107 | D07EF09C24576267004189AC /* WeatherSummary.swift */,
108 | D07EF09824575F3F004189AC /* WeatherError.swift */,
109 | );
110 | path = Models;
111 | sourceTree = "";
112 | };
113 | D07EF08E2456A29E004189AC /* Views */ = {
114 | isa = PBXGroup;
115 | children = (
116 | D07EF0B42457ED2E004189AC /* SearchBar */,
117 | D07EF0AF2457E12F004189AC /* DaySummary */,
118 | D07EF0AA2457D835004189AC /* HourSummary */,
119 | D07EF0A22457C187004189AC /* CurrentSummary */,
120 | D07EF08F2456A2AE004189AC /* WeatherSummary */,
121 | );
122 | path = Views;
123 | sourceTree = "";
124 | };
125 | D07EF08F2456A2AE004189AC /* WeatherSummary */ = {
126 | isa = PBXGroup;
127 | children = (
128 | D07EF08C2456A26B004189AC /* WeatherSummaryViewModel.swift */,
129 | D0ABD7492456993000840F37 /* WeatherSummaryView.swift */,
130 | );
131 | path = WeatherSummary;
132 | sourceTree = "";
133 | };
134 | D07EF0902456A2BC004189AC /* DataManager */ = {
135 | isa = PBXGroup;
136 | children = (
137 | D07EF0912456A2CA004189AC /* DataManager.swift */,
138 | );
139 | path = DataManager;
140 | sourceTree = "";
141 | };
142 | D07EF0932456A30F004189AC /* Utilities */ = {
143 | isa = PBXGroup;
144 | children = (
145 | D07EF0B72457F203004189AC /* DismissKeyboard.swift */,
146 | );
147 | path = Utilities;
148 | sourceTree = "";
149 | };
150 | D07EF0952456A6CC004189AC /* Mocks */ = {
151 | isa = PBXGroup;
152 | children = (
153 | D07EF0962456A6EE004189AC /* MockOneCallResponse.json */,
154 | D07EF09F24578E00004189AC /* MockWeatherFetcher.swift */,
155 | D07EF0A82457C2E9004189AC /* MockWeatherSummary.swift */,
156 | );
157 | path = Mocks;
158 | sourceTree = "";
159 | };
160 | D07EF0A22457C187004189AC /* CurrentSummary */ = {
161 | isa = PBXGroup;
162 | children = (
163 | D07EF0A32457C196004189AC /* CurrentSummaryViewModel.swift */,
164 | D07EF0A52457C1A7004189AC /* CurrentSummaryView.swift */,
165 | );
166 | path = CurrentSummary;
167 | sourceTree = "";
168 | };
169 | D07EF0AA2457D835004189AC /* HourSummary */ = {
170 | isa = PBXGroup;
171 | children = (
172 | D07EF0AB2457D83F004189AC /* HourSummaryViewModel.swift */,
173 | D07EF0AD2457D897004189AC /* HourSummaryView.swift */,
174 | );
175 | path = HourSummary;
176 | sourceTree = "";
177 | };
178 | D07EF0AF2457E12F004189AC /* DaySummary */ = {
179 | isa = PBXGroup;
180 | children = (
181 | D07EF0B22457E152004189AC /* DaySummaryViewModel.swift */,
182 | D07EF0B02457E13B004189AC /* DaySummaryView.swift */,
183 | );
184 | path = DaySummary;
185 | sourceTree = "";
186 | };
187 | D07EF0B42457ED2E004189AC /* SearchBar */ = {
188 | isa = PBXGroup;
189 | children = (
190 | D07EF0B52457ED40004189AC /* SearchBar.swift */,
191 | );
192 | path = SearchBar;
193 | sourceTree = "";
194 | };
195 | D0ABD7392456993000840F37 = {
196 | isa = PBXGroup;
197 | children = (
198 | D0ABD7442456993000840F37 /* Weather */,
199 | D0ABD75B2456993200840F37 /* WeatherTests */,
200 | D0ABD7432456993000840F37 /* Products */,
201 | );
202 | sourceTree = "";
203 | };
204 | D0ABD7432456993000840F37 /* Products */ = {
205 | isa = PBXGroup;
206 | children = (
207 | D0ABD7422456993000840F37 /* Weather.app */,
208 | D0ABD7582456993200840F37 /* WeatherTests.xctest */,
209 | );
210 | name = Products;
211 | sourceTree = "";
212 | };
213 | D0ABD7442456993000840F37 /* Weather */ = {
214 | isa = PBXGroup;
215 | children = (
216 | D0ABD7692456994D00840F37 /* Support */,
217 | D0ABD7682456994A00840F37 /* Sources */,
218 | D0ABD7672456993D00840F37 /* Resources */,
219 | );
220 | path = Weather;
221 | sourceTree = "";
222 | };
223 | D0ABD74D2456993200840F37 /* Preview Content */ = {
224 | isa = PBXGroup;
225 | children = (
226 | D0ABD74E2456993200840F37 /* Preview Assets.xcassets */,
227 | );
228 | path = "Preview Content";
229 | sourceTree = "";
230 | };
231 | D0ABD75B2456993200840F37 /* WeatherTests */ = {
232 | isa = PBXGroup;
233 | children = (
234 | D0ABD75C2456993200840F37 /* WeatherTests.swift */,
235 | D0ABD75E2456993200840F37 /* Info.plist */,
236 | D07EF0B92457F5AD004189AC /* DaySummaryViewModelTests.swift */,
237 | D07EF0BD2457F742004189AC /* HourSummaryViewModelTests.swift */,
238 | D07EF0BF2457F869004189AC /* CurrentSummaryViewModelTests.swift */,
239 | D07EF0C12457FA12004189AC /* WeatherSummaryViewModelTests.swift */,
240 | );
241 | path = WeatherTests;
242 | sourceTree = "";
243 | };
244 | D0ABD7672456993D00840F37 /* Resources */ = {
245 | isa = PBXGroup;
246 | children = (
247 | D07EF0952456A6CC004189AC /* Mocks */,
248 | D0ABD74B2456993200840F37 /* Assets.xcassets */,
249 | D0ABD7502456993200840F37 /* LaunchScreen.storyboard */,
250 | D0ABD74D2456993200840F37 /* Preview Content */,
251 | );
252 | path = Resources;
253 | sourceTree = "";
254 | };
255 | D0ABD7682456994A00840F37 /* Sources */ = {
256 | isa = PBXGroup;
257 | children = (
258 | D07EF0902456A2BC004189AC /* DataManager */,
259 | D07EF0892456A095004189AC /* Models */,
260 | D07EF08E2456A29E004189AC /* Views */,
261 | D07EF0932456A30F004189AC /* Utilities */,
262 | );
263 | path = Sources;
264 | sourceTree = "";
265 | };
266 | D0ABD7692456994D00840F37 /* Support */ = {
267 | isa = PBXGroup;
268 | children = (
269 | D0ABD7452456993000840F37 /* AppDelegate.swift */,
270 | D0ABD7472456993000840F37 /* SceneDelegate.swift */,
271 | D0ABD7532456993200840F37 /* Info.plist */,
272 | D07EF09A24575FCB004189AC /* Secrets.plist */,
273 | );
274 | path = Support;
275 | sourceTree = "";
276 | };
277 | /* End PBXGroup section */
278 |
279 | /* Begin PBXNativeTarget section */
280 | D0ABD7412456993000840F37 /* Weather */ = {
281 | isa = PBXNativeTarget;
282 | buildConfigurationList = D0ABD7612456993200840F37 /* Build configuration list for PBXNativeTarget "Weather" */;
283 | buildPhases = (
284 | D0ABD73E2456993000840F37 /* Sources */,
285 | D0ABD73F2456993000840F37 /* Frameworks */,
286 | D0ABD7402456993000840F37 /* Resources */,
287 | );
288 | buildRules = (
289 | );
290 | dependencies = (
291 | );
292 | name = Weather;
293 | productName = Weather;
294 | productReference = D0ABD7422456993000840F37 /* Weather.app */;
295 | productType = "com.apple.product-type.application";
296 | };
297 | D0ABD7572456993200840F37 /* WeatherTests */ = {
298 | isa = PBXNativeTarget;
299 | buildConfigurationList = D0ABD7642456993200840F37 /* Build configuration list for PBXNativeTarget "WeatherTests" */;
300 | buildPhases = (
301 | D0ABD7542456993200840F37 /* Sources */,
302 | D0ABD7552456993200840F37 /* Frameworks */,
303 | D0ABD7562456993200840F37 /* Resources */,
304 | );
305 | buildRules = (
306 | );
307 | dependencies = (
308 | D0ABD75A2456993200840F37 /* PBXTargetDependency */,
309 | );
310 | name = WeatherTests;
311 | productName = WeatherTests;
312 | productReference = D0ABD7582456993200840F37 /* WeatherTests.xctest */;
313 | productType = "com.apple.product-type.bundle.unit-test";
314 | };
315 | /* End PBXNativeTarget section */
316 |
317 | /* Begin PBXProject section */
318 | D0ABD73A2456993000840F37 /* Project object */ = {
319 | isa = PBXProject;
320 | attributes = {
321 | LastSwiftUpdateCheck = 1140;
322 | LastUpgradeCheck = 1140;
323 | ORGANIZATIONNAME = "Alex Fargo";
324 | TargetAttributes = {
325 | D0ABD7412456993000840F37 = {
326 | CreatedOnToolsVersion = 11.4.1;
327 | };
328 | D0ABD7572456993200840F37 = {
329 | CreatedOnToolsVersion = 11.4.1;
330 | TestTargetID = D0ABD7412456993000840F37;
331 | };
332 | };
333 | };
334 | buildConfigurationList = D0ABD73D2456993000840F37 /* Build configuration list for PBXProject "Weather" */;
335 | compatibilityVersion = "Xcode 9.3";
336 | developmentRegion = en;
337 | hasScannedForEncodings = 0;
338 | knownRegions = (
339 | en,
340 | Base,
341 | );
342 | mainGroup = D0ABD7392456993000840F37;
343 | productRefGroup = D0ABD7432456993000840F37 /* Products */;
344 | projectDirPath = "";
345 | projectRoot = "";
346 | targets = (
347 | D0ABD7412456993000840F37 /* Weather */,
348 | D0ABD7572456993200840F37 /* WeatherTests */,
349 | );
350 | };
351 | /* End PBXProject section */
352 |
353 | /* Begin PBXResourcesBuildPhase section */
354 | D0ABD7402456993000840F37 /* Resources */ = {
355 | isa = PBXResourcesBuildPhase;
356 | buildActionMask = 2147483647;
357 | files = (
358 | D0ABD7522456993200840F37 /* LaunchScreen.storyboard in Resources */,
359 | D07EF09B24575FCB004189AC /* Secrets.plist in Resources */,
360 | D0ABD74F2456993200840F37 /* Preview Assets.xcassets in Resources */,
361 | D0ABD74C2456993200840F37 /* Assets.xcassets in Resources */,
362 | D07EF0972456A6EE004189AC /* MockOneCallResponse.json in Resources */,
363 | );
364 | runOnlyForDeploymentPostprocessing = 0;
365 | };
366 | D0ABD7562456993200840F37 /* Resources */ = {
367 | isa = PBXResourcesBuildPhase;
368 | buildActionMask = 2147483647;
369 | files = (
370 | );
371 | runOnlyForDeploymentPostprocessing = 0;
372 | };
373 | /* End PBXResourcesBuildPhase section */
374 |
375 | /* Begin PBXSourcesBuildPhase section */
376 | D0ABD73E2456993000840F37 /* Sources */ = {
377 | isa = PBXSourcesBuildPhase;
378 | buildActionMask = 2147483647;
379 | files = (
380 | D07EF08B2456A0A4004189AC /* OneCallResponse.swift in Sources */,
381 | D07EF0AC2457D83F004189AC /* HourSummaryViewModel.swift in Sources */,
382 | D07EF0922456A2CA004189AC /* DataManager.swift in Sources */,
383 | D07EF09E24578D6B004189AC /* WeatherSummaryViewModel.swift in Sources */,
384 | D07EF0A92457C2E9004189AC /* MockWeatherSummary.swift in Sources */,
385 | D07EF0AE2457D897004189AC /* HourSummaryView.swift in Sources */,
386 | D0ABD7462456993000840F37 /* AppDelegate.swift in Sources */,
387 | D0ABD7482456993000840F37 /* SceneDelegate.swift in Sources */,
388 | D07EF0942456A383004189AC /* WeatherSummaryView.swift in Sources */,
389 | D07EF0A024578E00004189AC /* MockWeatherFetcher.swift in Sources */,
390 | D07EF0B12457E13B004189AC /* DaySummaryView.swift in Sources */,
391 | D07EF0B82457F203004189AC /* DismissKeyboard.swift in Sources */,
392 | D07EF0A62457C1A7004189AC /* CurrentSummaryView.swift in Sources */,
393 | D07EF0B32457E152004189AC /* DaySummaryViewModel.swift in Sources */,
394 | D07EF09D24576267004189AC /* WeatherSummary.swift in Sources */,
395 | D07EF09924575F3F004189AC /* WeatherError.swift in Sources */,
396 | D07EF0B62457ED40004189AC /* SearchBar.swift in Sources */,
397 | D07EF0A42457C196004189AC /* CurrentSummaryViewModel.swift in Sources */,
398 | );
399 | runOnlyForDeploymentPostprocessing = 0;
400 | };
401 | D0ABD7542456993200840F37 /* Sources */ = {
402 | isa = PBXSourcesBuildPhase;
403 | buildActionMask = 2147483647;
404 | files = (
405 | D07EF0C22457FA12004189AC /* WeatherSummaryViewModelTests.swift in Sources */,
406 | D07EF0BA2457F5AD004189AC /* DaySummaryViewModelTests.swift in Sources */,
407 | D07EF0C02457F869004189AC /* CurrentSummaryViewModelTests.swift in Sources */,
408 | D0ABD75D2456993200840F37 /* WeatherTests.swift in Sources */,
409 | D07EF0BE2457F742004189AC /* HourSummaryViewModelTests.swift in Sources */,
410 | );
411 | runOnlyForDeploymentPostprocessing = 0;
412 | };
413 | /* End PBXSourcesBuildPhase section */
414 |
415 | /* Begin PBXTargetDependency section */
416 | D0ABD75A2456993200840F37 /* PBXTargetDependency */ = {
417 | isa = PBXTargetDependency;
418 | target = D0ABD7412456993000840F37 /* Weather */;
419 | targetProxy = D0ABD7592456993200840F37 /* PBXContainerItemProxy */;
420 | };
421 | /* End PBXTargetDependency section */
422 |
423 | /* Begin PBXVariantGroup section */
424 | D0ABD7502456993200840F37 /* LaunchScreen.storyboard */ = {
425 | isa = PBXVariantGroup;
426 | children = (
427 | D0ABD7512456993200840F37 /* Base */,
428 | );
429 | name = LaunchScreen.storyboard;
430 | sourceTree = "";
431 | };
432 | /* End PBXVariantGroup section */
433 |
434 | /* Begin XCBuildConfiguration section */
435 | D0ABD75F2456993200840F37 /* Debug */ = {
436 | isa = XCBuildConfiguration;
437 | buildSettings = {
438 | ALWAYS_SEARCH_USER_PATHS = NO;
439 | CLANG_ANALYZER_NONNULL = YES;
440 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
441 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
442 | CLANG_CXX_LIBRARY = "libc++";
443 | CLANG_ENABLE_MODULES = YES;
444 | CLANG_ENABLE_OBJC_ARC = YES;
445 | CLANG_ENABLE_OBJC_WEAK = YES;
446 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
447 | CLANG_WARN_BOOL_CONVERSION = YES;
448 | CLANG_WARN_COMMA = YES;
449 | CLANG_WARN_CONSTANT_CONVERSION = YES;
450 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
451 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
452 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
453 | CLANG_WARN_EMPTY_BODY = YES;
454 | CLANG_WARN_ENUM_CONVERSION = YES;
455 | CLANG_WARN_INFINITE_RECURSION = YES;
456 | CLANG_WARN_INT_CONVERSION = YES;
457 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
458 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
459 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
460 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
461 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
462 | CLANG_WARN_STRICT_PROTOTYPES = YES;
463 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
464 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
465 | CLANG_WARN_UNREACHABLE_CODE = YES;
466 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
467 | COPY_PHASE_STRIP = NO;
468 | DEBUG_INFORMATION_FORMAT = dwarf;
469 | ENABLE_STRICT_OBJC_MSGSEND = YES;
470 | ENABLE_TESTABILITY = YES;
471 | GCC_C_LANGUAGE_STANDARD = gnu11;
472 | GCC_DYNAMIC_NO_PIC = NO;
473 | GCC_NO_COMMON_BLOCKS = YES;
474 | GCC_OPTIMIZATION_LEVEL = 0;
475 | GCC_PREPROCESSOR_DEFINITIONS = (
476 | "DEBUG=1",
477 | "$(inherited)",
478 | );
479 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
480 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
481 | GCC_WARN_UNDECLARED_SELECTOR = YES;
482 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
483 | GCC_WARN_UNUSED_FUNCTION = YES;
484 | GCC_WARN_UNUSED_VARIABLE = YES;
485 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
486 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
487 | MTL_FAST_MATH = YES;
488 | ONLY_ACTIVE_ARCH = YES;
489 | SDKROOT = iphoneos;
490 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
491 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
492 | };
493 | name = Debug;
494 | };
495 | D0ABD7602456993200840F37 /* Release */ = {
496 | isa = XCBuildConfiguration;
497 | buildSettings = {
498 | ALWAYS_SEARCH_USER_PATHS = NO;
499 | CLANG_ANALYZER_NONNULL = YES;
500 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
501 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
502 | CLANG_CXX_LIBRARY = "libc++";
503 | CLANG_ENABLE_MODULES = YES;
504 | CLANG_ENABLE_OBJC_ARC = YES;
505 | CLANG_ENABLE_OBJC_WEAK = YES;
506 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
507 | CLANG_WARN_BOOL_CONVERSION = YES;
508 | CLANG_WARN_COMMA = YES;
509 | CLANG_WARN_CONSTANT_CONVERSION = YES;
510 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
511 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
512 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
513 | CLANG_WARN_EMPTY_BODY = YES;
514 | CLANG_WARN_ENUM_CONVERSION = YES;
515 | CLANG_WARN_INFINITE_RECURSION = YES;
516 | CLANG_WARN_INT_CONVERSION = YES;
517 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
518 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
519 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
520 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
521 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
522 | CLANG_WARN_STRICT_PROTOTYPES = YES;
523 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
524 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
525 | CLANG_WARN_UNREACHABLE_CODE = YES;
526 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
527 | COPY_PHASE_STRIP = NO;
528 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
529 | ENABLE_NS_ASSERTIONS = NO;
530 | ENABLE_STRICT_OBJC_MSGSEND = YES;
531 | GCC_C_LANGUAGE_STANDARD = gnu11;
532 | GCC_NO_COMMON_BLOCKS = YES;
533 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
534 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
535 | GCC_WARN_UNDECLARED_SELECTOR = YES;
536 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
537 | GCC_WARN_UNUSED_FUNCTION = YES;
538 | GCC_WARN_UNUSED_VARIABLE = YES;
539 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
540 | MTL_ENABLE_DEBUG_INFO = NO;
541 | MTL_FAST_MATH = YES;
542 | SDKROOT = iphoneos;
543 | SWIFT_COMPILATION_MODE = wholemodule;
544 | SWIFT_OPTIMIZATION_LEVEL = "-O";
545 | VALIDATE_PRODUCT = YES;
546 | };
547 | name = Release;
548 | };
549 | D0ABD7622456993200840F37 /* Debug */ = {
550 | isa = XCBuildConfiguration;
551 | buildSettings = {
552 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
553 | CODE_SIGN_STYLE = Automatic;
554 | DEVELOPMENT_ASSET_PATHS = "\"Weather/Resources/Preview Content\"";
555 | DEVELOPMENT_TEAM = AEXVC7BMBG;
556 | ENABLE_PREVIEWS = YES;
557 | INFOPLIST_FILE = Weather/Support/Info.plist;
558 | LD_RUNPATH_SEARCH_PATHS = (
559 | "$(inherited)",
560 | "@executable_path/Frameworks",
561 | );
562 | PRODUCT_BUNDLE_IDENTIFIER = xyz.alexfargo.Weather;
563 | PRODUCT_NAME = "$(TARGET_NAME)";
564 | SWIFT_VERSION = 5.0;
565 | TARGETED_DEVICE_FAMILY = "1,2";
566 | };
567 | name = Debug;
568 | };
569 | D0ABD7632456993200840F37 /* Release */ = {
570 | isa = XCBuildConfiguration;
571 | buildSettings = {
572 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
573 | CODE_SIGN_STYLE = Automatic;
574 | DEVELOPMENT_ASSET_PATHS = "\"Weather/Resources/Preview Content\"";
575 | DEVELOPMENT_TEAM = AEXVC7BMBG;
576 | ENABLE_PREVIEWS = YES;
577 | INFOPLIST_FILE = Weather/Support/Info.plist;
578 | LD_RUNPATH_SEARCH_PATHS = (
579 | "$(inherited)",
580 | "@executable_path/Frameworks",
581 | );
582 | PRODUCT_BUNDLE_IDENTIFIER = xyz.alexfargo.Weather;
583 | PRODUCT_NAME = "$(TARGET_NAME)";
584 | SWIFT_VERSION = 5.0;
585 | TARGETED_DEVICE_FAMILY = "1,2";
586 | };
587 | name = Release;
588 | };
589 | D0ABD7652456993200840F37 /* Debug */ = {
590 | isa = XCBuildConfiguration;
591 | buildSettings = {
592 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
593 | BUNDLE_LOADER = "$(TEST_HOST)";
594 | CODE_SIGN_STYLE = Automatic;
595 | DEVELOPMENT_TEAM = AEXVC7BMBG;
596 | INFOPLIST_FILE = WeatherTests/Info.plist;
597 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
598 | LD_RUNPATH_SEARCH_PATHS = (
599 | "$(inherited)",
600 | "@executable_path/Frameworks",
601 | "@loader_path/Frameworks",
602 | );
603 | PRODUCT_BUNDLE_IDENTIFIER = xyz.alexfargo.WeatherTests;
604 | PRODUCT_NAME = "$(TARGET_NAME)";
605 | SWIFT_VERSION = 5.0;
606 | TARGETED_DEVICE_FAMILY = "1,2";
607 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Weather.app/Weather";
608 | };
609 | name = Debug;
610 | };
611 | D0ABD7662456993200840F37 /* Release */ = {
612 | isa = XCBuildConfiguration;
613 | buildSettings = {
614 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
615 | BUNDLE_LOADER = "$(TEST_HOST)";
616 | CODE_SIGN_STYLE = Automatic;
617 | DEVELOPMENT_TEAM = AEXVC7BMBG;
618 | INFOPLIST_FILE = WeatherTests/Info.plist;
619 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
620 | LD_RUNPATH_SEARCH_PATHS = (
621 | "$(inherited)",
622 | "@executable_path/Frameworks",
623 | "@loader_path/Frameworks",
624 | );
625 | PRODUCT_BUNDLE_IDENTIFIER = xyz.alexfargo.WeatherTests;
626 | PRODUCT_NAME = "$(TARGET_NAME)";
627 | SWIFT_VERSION = 5.0;
628 | TARGETED_DEVICE_FAMILY = "1,2";
629 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Weather.app/Weather";
630 | };
631 | name = Release;
632 | };
633 | /* End XCBuildConfiguration section */
634 |
635 | /* Begin XCConfigurationList section */
636 | D0ABD73D2456993000840F37 /* Build configuration list for PBXProject "Weather" */ = {
637 | isa = XCConfigurationList;
638 | buildConfigurations = (
639 | D0ABD75F2456993200840F37 /* Debug */,
640 | D0ABD7602456993200840F37 /* Release */,
641 | );
642 | defaultConfigurationIsVisible = 0;
643 | defaultConfigurationName = Release;
644 | };
645 | D0ABD7612456993200840F37 /* Build configuration list for PBXNativeTarget "Weather" */ = {
646 | isa = XCConfigurationList;
647 | buildConfigurations = (
648 | D0ABD7622456993200840F37 /* Debug */,
649 | D0ABD7632456993200840F37 /* Release */,
650 | );
651 | defaultConfigurationIsVisible = 0;
652 | defaultConfigurationName = Release;
653 | };
654 | D0ABD7642456993200840F37 /* Build configuration list for PBXNativeTarget "WeatherTests" */ = {
655 | isa = XCConfigurationList;
656 | buildConfigurations = (
657 | D0ABD7652456993200840F37 /* Debug */,
658 | D0ABD7662456993200840F37 /* Release */,
659 | );
660 | defaultConfigurationIsVisible = 0;
661 | defaultConfigurationName = Release;
662 | };
663 | /* End XCConfigurationList section */
664 | };
665 | rootObject = D0ABD73A2456993000840F37 /* Project object */;
666 | }
667 |
--------------------------------------------------------------------------------
/Weather.xcodeproj/xcshareddata/xcschemes/Weather.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
71 |
73 |
79 |
80 |
81 |
82 |
84 |
85 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/Weather/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Weather/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Weather/Resources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Weather/Resources/Mocks/MockOneCallResponse.json:
--------------------------------------------------------------------------------
1 | {
2 | "lat": 34.15,
3 | "lon": -118.14,
4 | "timezone": "America/Los_Angeles",
5 | "current": {
6 | "dt": 1588013546,
7 | "sunrise": 1587992793,
8 | "sunset": 1588041202,
9 | "temp": 299.04,
10 | "feels_like": 299.39,
11 | "pressure": 1015,
12 | "humidity": 53,
13 | "dew_point": 288.74,
14 | "uvi": 9.72,
15 | "clouds": 1,
16 | "visibility": 12874,
17 | "wind_speed": 2.1,
18 | "wind_deg": 200,
19 | "weather": [
20 | {
21 | "id": 800,
22 | "main": "Clear",
23 | "description": "clear sky",
24 | "icon": "01d"
25 | }
26 | ]
27 | },
28 | "hourly": [
29 | {
30 | "dt": 1588010400,
31 | "temp": 299.04,
32 | "feels_like": 299,
33 | "pressure": 1015,
34 | "humidity": 53,
35 | "dew_point": 288.74,
36 | "clouds": 1,
37 | "wind_speed": 2.66,
38 | "wind_deg": 221,
39 | "weather": [
40 | {
41 | "id": 800,
42 | "main": "Clear",
43 | "description": "clear sky",
44 | "icon": "01d"
45 | }
46 | ]
47 | },
48 | {
49 | "dt": 1588014000,
50 | "temp": 299.94,
51 | "feels_like": 298.38,
52 | "pressure": 1015,
53 | "humidity": 40,
54 | "dew_point": 285.22,
55 | "clouds": 1,
56 | "wind_speed": 3.14,
57 | "wind_deg": 220,
58 | "weather": [
59 | {
60 | "id": 800,
61 | "main": "Clear",
62 | "description": "clear sky",
63 | "icon": "01d"
64 | }
65 | ]
66 | },
67 | {
68 | "dt": 1588017600,
69 | "temp": 301.16,
70 | "feels_like": 298.65,
71 | "pressure": 1014,
72 | "humidity": 31,
73 | "dew_point": 282.47,
74 | "clouds": 0,
75 | "wind_speed": 3.38,
76 | "wind_deg": 218,
77 | "weather": [
78 | {
79 | "id": 800,
80 | "main": "Clear",
81 | "description": "clear sky",
82 | "icon": "01d"
83 | }
84 | ]
85 | },
86 | {
87 | "dt": 1588021200,
88 | "temp": 302.23,
89 | "feels_like": 299.09,
90 | "pressure": 1014,
91 | "humidity": 25,
92 | "dew_point": 280.22,
93 | "clouds": 0,
94 | "wind_speed": 3.5,
95 | "wind_deg": 220,
96 | "weather": [
97 | {
98 | "id": 800,
99 | "main": "Clear",
100 | "description": "clear sky",
101 | "icon": "01d"
102 | }
103 | ]
104 | },
105 | {
106 | "dt": 1588024800,
107 | "temp": 302.58,
108 | "feels_like": 299.27,
109 | "pressure": 1014,
110 | "humidity": 24,
111 | "dew_point": 279.92,
112 | "clouds": 0,
113 | "wind_speed": 3.64,
114 | "wind_deg": 221,
115 | "weather": [
116 | {
117 | "id": 800,
118 | "main": "Clear",
119 | "description": "clear sky",
120 | "icon": "01d"
121 | }
122 | ]
123 | },
124 | {
125 | "dt": 1588028400,
126 | "temp": 302.11,
127 | "feels_like": 298.9,
128 | "pressure": 1013,
129 | "humidity": 26,
130 | "dew_point": 282,
131 | "clouds": 0,
132 | "wind_speed": 3.75,
133 | "wind_deg": 221,
134 | "weather": [
135 | {
136 | "id": 800,
137 | "main": "Clear",
138 | "description": "clear sky",
139 | "icon": "01d"
140 | }
141 | ]
142 | },
143 | {
144 | "dt": 1588032000,
145 | "temp": 301.21,
146 | "feels_like": 298.32,
147 | "pressure": 1013,
148 | "humidity": 29,
149 | "dew_point": 282.57,
150 | "clouds": 0,
151 | "wind_speed": 3.58,
152 | "wind_deg": 221,
153 | "weather": [
154 | {
155 | "id": 800,
156 | "main": "Clear",
157 | "description": "clear sky",
158 | "icon": "01d"
159 | }
160 | ]
161 | },
162 | {
163 | "dt": 1588035600,
164 | "temp": 299.97,
165 | "feels_like": 297.49,
166 | "pressure": 1013,
167 | "humidity": 32,
168 | "dew_point": 283.06,
169 | "clouds": 0,
170 | "wind_speed": 3.14,
171 | "wind_deg": 218,
172 | "weather": [
173 | {
174 | "id": 800,
175 | "main": "Clear",
176 | "description": "clear sky",
177 | "icon": "01d"
178 | }
179 | ]
180 | },
181 | {
182 | "dt": 1588039200,
183 | "temp": 298.27,
184 | "feels_like": 296.56,
185 | "pressure": 1013,
186 | "humidity": 37,
187 | "dew_point": 283.68,
188 | "clouds": 0,
189 | "wind_speed": 2.28,
190 | "wind_deg": 218,
191 | "weather": [
192 | {
193 | "id": 800,
194 | "main": "Clear",
195 | "description": "clear sky",
196 | "icon": "01d"
197 | }
198 | ]
199 | },
200 | {
201 | "dt": 1588042800,
202 | "temp": 296.58,
203 | "feels_like": 295.46,
204 | "pressure": 1014,
205 | "humidity": 39,
206 | "dew_point": 283.26,
207 | "clouds": 0,
208 | "wind_speed": 1.17,
209 | "wind_deg": 216,
210 | "weather": [
211 | {
212 | "id": 800,
213 | "main": "Clear",
214 | "description": "clear sky",
215 | "icon": "01n"
216 | }
217 | ]
218 | },
219 | {
220 | "dt": 1588046400,
221 | "temp": 296.19,
222 | "feels_like": 295.47,
223 | "pressure": 1015,
224 | "humidity": 39,
225 | "dew_point": 282.64,
226 | "clouds": 0,
227 | "wind_speed": 0.48,
228 | "wind_deg": 186,
229 | "weather": [
230 | {
231 | "id": 800,
232 | "main": "Clear",
233 | "description": "clear sky",
234 | "icon": "01n"
235 | }
236 | ]
237 | },
238 | {
239 | "dt": 1588050000,
240 | "temp": 295.83,
241 | "feels_like": 295.07,
242 | "pressure": 1015,
243 | "humidity": 38,
244 | "dew_point": 281.97,
245 | "clouds": 0,
246 | "wind_speed": 0.3,
247 | "wind_deg": 143,
248 | "weather": [
249 | {
250 | "id": 800,
251 | "main": "Clear",
252 | "description": "clear sky",
253 | "icon": "01n"
254 | }
255 | ]
256 | },
257 | {
258 | "dt": 1588053600,
259 | "temp": 295.38,
260 | "feels_like": 294.48,
261 | "pressure": 1015,
262 | "humidity": 38,
263 | "dew_point": 281.48,
264 | "clouds": 0,
265 | "wind_speed": 0.36,
266 | "wind_deg": 125,
267 | "weather": [
268 | {
269 | "id": 800,
270 | "main": "Clear",
271 | "description": "clear sky",
272 | "icon": "01n"
273 | }
274 | ]
275 | },
276 | {
277 | "dt": 1588057200,
278 | "temp": 295.05,
279 | "feels_like": 294.13,
280 | "pressure": 1015,
281 | "humidity": 37,
282 | "dew_point": 281,
283 | "clouds": 0,
284 | "wind_speed": 0.17,
285 | "wind_deg": 95,
286 | "weather": [
287 | {
288 | "id": 800,
289 | "main": "Clear",
290 | "description": "clear sky",
291 | "icon": "01n"
292 | }
293 | ]
294 | },
295 | {
296 | "dt": 1588060800,
297 | "temp": 294.71,
298 | "feels_like": 293.76,
299 | "pressure": 1015,
300 | "humidity": 37,
301 | "dew_point": 280.66,
302 | "clouds": 0,
303 | "wind_speed": 0.12,
304 | "wind_deg": 149,
305 | "weather": [
306 | {
307 | "id": 800,
308 | "main": "Clear",
309 | "description": "clear sky",
310 | "icon": "01n"
311 | }
312 | ]
313 | },
314 | {
315 | "dt": 1588064400,
316 | "temp": 294.29,
317 | "feels_like": 293.29,
318 | "pressure": 1015,
319 | "humidity": 38,
320 | "dew_point": 280.47,
321 | "clouds": 0,
322 | "wind_speed": 0.19,
323 | "wind_deg": 195,
324 | "weather": [
325 | {
326 | "id": 800,
327 | "main": "Clear",
328 | "description": "clear sky",
329 | "icon": "01n"
330 | }
331 | ]
332 | },
333 | {
334 | "dt": 1588068000,
335 | "temp": 293.92,
336 | "feels_like": 292.84,
337 | "pressure": 1014,
338 | "humidity": 38,
339 | "dew_point": 280.36,
340 | "clouds": 0,
341 | "wind_speed": 0.21,
342 | "wind_deg": 209,
343 | "weather": [
344 | {
345 | "id": 800,
346 | "main": "Clear",
347 | "description": "clear sky",
348 | "icon": "01n"
349 | }
350 | ]
351 | },
352 | {
353 | "dt": 1588071600,
354 | "temp": 293.7,
355 | "feels_like": 292.65,
356 | "pressure": 1014,
357 | "humidity": 38,
358 | "dew_point": 280.26,
359 | "clouds": 0,
360 | "wind_speed": 0.11,
361 | "wind_deg": 86,
362 | "weather": [
363 | {
364 | "id": 800,
365 | "main": "Clear",
366 | "description": "clear sky",
367 | "icon": "01n"
368 | }
369 | ]
370 | },
371 | {
372 | "dt": 1588075200,
373 | "temp": 293.57,
374 | "feels_like": 292.35,
375 | "pressure": 1014,
376 | "humidity": 38,
377 | "dew_point": 280.16,
378 | "clouds": 0,
379 | "wind_speed": 0.32,
380 | "wind_deg": 73,
381 | "weather": [
382 | {
383 | "id": 800,
384 | "main": "Clear",
385 | "description": "clear sky",
386 | "icon": "01n"
387 | }
388 | ]
389 | },
390 | {
391 | "dt": 1588078800,
392 | "temp": 293.48,
393 | "feels_like": 292.32,
394 | "pressure": 1014,
395 | "humidity": 38,
396 | "dew_point": 280.11,
397 | "clouds": 0,
398 | "wind_speed": 0.2,
399 | "wind_deg": 27,
400 | "weather": [
401 | {
402 | "id": 800,
403 | "main": "Clear",
404 | "description": "clear sky",
405 | "icon": "01n"
406 | }
407 | ]
408 | },
409 | {
410 | "dt": 1588082400,
411 | "temp": 294.26,
412 | "feels_like": 293.23,
413 | "pressure": 1015,
414 | "humidity": 38,
415 | "dew_point": 280.79,
416 | "clouds": 0,
417 | "wind_speed": 0.23,
418 | "wind_deg": 337,
419 | "weather": [
420 | {
421 | "id": 800,
422 | "main": "Clear",
423 | "description": "clear sky",
424 | "icon": "01d"
425 | }
426 | ]
427 | },
428 | {
429 | "dt": 1588086000,
430 | "temp": 296.45,
431 | "feels_like": 295.29,
432 | "pressure": 1015,
433 | "humidity": 34,
434 | "dew_point": 281.14,
435 | "clouds": 0,
436 | "wind_speed": 0.52,
437 | "wind_deg": 224,
438 | "weather": [
439 | {
440 | "id": 800,
441 | "main": "Clear",
442 | "description": "clear sky",
443 | "icon": "01d"
444 | }
445 | ]
446 | },
447 | {
448 | "dt": 1588089600,
449 | "temp": 298.42,
450 | "feels_like": 296.64,
451 | "pressure": 1015,
452 | "humidity": 30,
453 | "dew_point": 281.03,
454 | "clouds": 0,
455 | "wind_speed": 1.37,
456 | "wind_deg": 228,
457 | "weather": [
458 | {
459 | "id": 800,
460 | "main": "Clear",
461 | "description": "clear sky",
462 | "icon": "01d"
463 | }
464 | ]
465 | },
466 | {
467 | "dt": 1588093200,
468 | "temp": 300.03,
469 | "feels_like": 297.97,
470 | "pressure": 1015,
471 | "humidity": 28,
472 | "dew_point": 281.06,
473 | "clouds": 0,
474 | "wind_speed": 1.88,
475 | "wind_deg": 223,
476 | "weather": [
477 | {
478 | "id": 800,
479 | "main": "Clear",
480 | "description": "clear sky",
481 | "icon": "01d"
482 | }
483 | ]
484 | },
485 | {
486 | "dt": 1588096800,
487 | "temp": 301.57,
488 | "feels_like": 299.16,
489 | "pressure": 1015,
490 | "humidity": 25,
491 | "dew_point": 281.01,
492 | "clouds": 0,
493 | "wind_speed": 2.28,
494 | "wind_deg": 223,
495 | "weather": [
496 | {
497 | "id": 800,
498 | "main": "Clear",
499 | "description": "clear sky",
500 | "icon": "01d"
501 | }
502 | ]
503 | },
504 | {
505 | "dt": 1588100400,
506 | "temp": 303.01,
507 | "feels_like": 300.25,
508 | "pressure": 1015,
509 | "humidity": 23,
510 | "dew_point": 280.9,
511 | "clouds": 0,
512 | "wind_speed": 2.78,
513 | "wind_deg": 222,
514 | "weather": [
515 | {
516 | "id": 800,
517 | "main": "Clear",
518 | "description": "clear sky",
519 | "icon": "01d"
520 | }
521 | ]
522 | },
523 | {
524 | "dt": 1588104000,
525 | "temp": 303.9,
526 | "feels_like": 300.77,
527 | "pressure": 1014,
528 | "humidity": 22,
529 | "dew_point": 281.11,
530 | "clouds": 0,
531 | "wind_speed": 3.34,
532 | "wind_deg": 223,
533 | "weather": [
534 | {
535 | "id": 800,
536 | "main": "Clear",
537 | "description": "clear sky",
538 | "icon": "01d"
539 | }
540 | ]
541 | },
542 | {
543 | "dt": 1588107600,
544 | "temp": 304.31,
545 | "feels_like": 301.09,
546 | "pressure": 1014,
547 | "humidity": 22,
548 | "dew_point": 281.43,
549 | "clouds": 0,
550 | "wind_speed": 3.57,
551 | "wind_deg": 223,
552 | "weather": [
553 | {
554 | "id": 800,
555 | "main": "Clear",
556 | "description": "clear sky",
557 | "icon": "01d"
558 | }
559 | ]
560 | },
561 | {
562 | "dt": 1588111200,
563 | "temp": 304.36,
564 | "feels_like": 301.09,
565 | "pressure": 1014,
566 | "humidity": 22,
567 | "dew_point": 281.64,
568 | "clouds": 0,
569 | "wind_speed": 3.66,
570 | "wind_deg": 223,
571 | "weather": [
572 | {
573 | "id": 800,
574 | "main": "Clear",
575 | "description": "clear sky",
576 | "icon": "01d"
577 | }
578 | ]
579 | },
580 | {
581 | "dt": 1588114800,
582 | "temp": 304,
583 | "feels_like": 300.86,
584 | "pressure": 1013,
585 | "humidity": 23,
586 | "dew_point": 281.76,
587 | "clouds": 0,
588 | "wind_speed": 3.58,
589 | "wind_deg": 227,
590 | "weather": [
591 | {
592 | "id": 800,
593 | "main": "Clear",
594 | "description": "clear sky",
595 | "icon": "01d"
596 | }
597 | ]
598 | },
599 | {
600 | "dt": 1588118400,
601 | "temp": 303.34,
602 | "feels_like": 300.43,
603 | "pressure": 1013,
604 | "humidity": 24,
605 | "dew_point": 281.92,
606 | "clouds": 0,
607 | "wind_speed": 3.28,
608 | "wind_deg": 224,
609 | "weather": [
610 | {
611 | "id": 800,
612 | "main": "Clear",
613 | "description": "clear sky",
614 | "icon": "01d"
615 | }
616 | ]
617 | },
618 | {
619 | "dt": 1588122000,
620 | "temp": 302.12,
621 | "feels_like": 299.6,
622 | "pressure": 1012,
623 | "humidity": 27,
624 | "dew_point": 282.33,
625 | "clouds": 0,
626 | "wind_speed": 2.96,
627 | "wind_deg": 216,
628 | "weather": [
629 | {
630 | "id": 800,
631 | "main": "Clear",
632 | "description": "clear sky",
633 | "icon": "01d"
634 | }
635 | ]
636 | },
637 | {
638 | "dt": 1588125600,
639 | "temp": 299.96,
640 | "feels_like": 298.11,
641 | "pressure": 1013,
642 | "humidity": 32,
643 | "dew_point": 283.07,
644 | "clouds": 0,
645 | "wind_speed": 2.23,
646 | "wind_deg": 209,
647 | "weather": [
648 | {
649 | "id": 800,
650 | "main": "Clear",
651 | "description": "clear sky",
652 | "icon": "01d"
653 | }
654 | ]
655 | },
656 | {
657 | "dt": 1588129200,
658 | "temp": 298.14,
659 | "feels_like": 296.82,
660 | "pressure": 1013,
661 | "humidity": 35,
662 | "dew_point": 282.68,
663 | "clouds": 0,
664 | "wind_speed": 1.38,
665 | "wind_deg": 185,
666 | "weather": [
667 | {
668 | "id": 800,
669 | "main": "Clear",
670 | "description": "clear sky",
671 | "icon": "01n"
672 | }
673 | ]
674 | },
675 | {
676 | "dt": 1588132800,
677 | "temp": 297.5,
678 | "feels_like": 296.38,
679 | "pressure": 1014,
680 | "humidity": 36,
681 | "dew_point": 282.64,
682 | "clouds": 0,
683 | "wind_speed": 1.04,
684 | "wind_deg": 175,
685 | "weather": [
686 | {
687 | "id": 800,
688 | "main": "Clear",
689 | "description": "clear sky",
690 | "icon": "01n"
691 | }
692 | ]
693 | },
694 | {
695 | "dt": 1588136400,
696 | "temp": 297.09,
697 | "feels_like": 296.18,
698 | "pressure": 1015,
699 | "humidity": 37,
700 | "dew_point": 282.57,
701 | "clouds": 0,
702 | "wind_speed": 0.75,
703 | "wind_deg": 157,
704 | "weather": [
705 | {
706 | "id": 800,
707 | "main": "Clear",
708 | "description": "clear sky",
709 | "icon": "01n"
710 | }
711 | ]
712 | },
713 | {
714 | "dt": 1588140000,
715 | "temp": 296.84,
716 | "feels_like": 295.94,
717 | "pressure": 1015,
718 | "humidity": 37,
719 | "dew_point": 282.47,
720 | "clouds": 0,
721 | "wind_speed": 0.66,
722 | "wind_deg": 162,
723 | "weather": [
724 | {
725 | "id": 800,
726 | "main": "Clear",
727 | "description": "clear sky",
728 | "icon": "01n"
729 | }
730 | ]
731 | },
732 | {
733 | "dt": 1588143600,
734 | "temp": 296.31,
735 | "feels_like": 295.48,
736 | "pressure": 1015,
737 | "humidity": 38,
738 | "dew_point": 282.39,
739 | "clouds": 0,
740 | "wind_speed": 0.54,
741 | "wind_deg": 198,
742 | "weather": [
743 | {
744 | "id": 800,
745 | "main": "Clear",
746 | "description": "clear sky",
747 | "icon": "01n"
748 | }
749 | ]
750 | },
751 | {
752 | "dt": 1588147200,
753 | "temp": 295.97,
754 | "feels_like": 295.2,
755 | "pressure": 1014,
756 | "humidity": 38,
757 | "dew_point": 282.2,
758 | "clouds": 0,
759 | "wind_speed": 0.35,
760 | "wind_deg": 211,
761 | "weather": [
762 | {
763 | "id": 800,
764 | "main": "Clear",
765 | "description": "clear sky",
766 | "icon": "01n"
767 | }
768 | ]
769 | },
770 | {
771 | "dt": 1588150800,
772 | "temp": 295.69,
773 | "feels_like": 294.62,
774 | "pressure": 1013,
775 | "humidity": 38,
776 | "dew_point": 281.89,
777 | "clouds": 0,
778 | "wind_speed": 0.7,
779 | "wind_deg": 244,
780 | "weather": [
781 | {
782 | "id": 800,
783 | "main": "Clear",
784 | "description": "clear sky",
785 | "icon": "01n"
786 | }
787 | ]
788 | },
789 | {
790 | "dt": 1588154400,
791 | "temp": 295.36,
792 | "feels_like": 294.53,
793 | "pressure": 1013,
794 | "humidity": 38,
795 | "dew_point": 281.62,
796 | "clouds": 0,
797 | "wind_speed": 0.25,
798 | "wind_deg": 235,
799 | "weather": [
800 | {
801 | "id": 800,
802 | "main": "Clear",
803 | "description": "clear sky",
804 | "icon": "01n"
805 | }
806 | ]
807 | },
808 | {
809 | "dt": 1588158000,
810 | "temp": 295,
811 | "feels_like": 293.74,
812 | "pressure": 1013,
813 | "humidity": 38,
814 | "dew_point": 281.41,
815 | "clouds": 0,
816 | "wind_speed": 0.76,
817 | "wind_deg": 246,
818 | "weather": [
819 | {
820 | "id": 800,
821 | "main": "Clear",
822 | "description": "clear sky",
823 | "icon": "01n"
824 | }
825 | ]
826 | },
827 | {
828 | "dt": 1588161600,
829 | "temp": 295.01,
830 | "feels_like": 294.1,
831 | "pressure": 1013,
832 | "humidity": 37,
833 | "dew_point": 281.05,
834 | "clouds": 1,
835 | "wind_speed": 0.14,
836 | "wind_deg": 291,
837 | "weather": [
838 | {
839 | "id": 800,
840 | "main": "Clear",
841 | "description": "clear sky",
842 | "icon": "01n"
843 | }
844 | ]
845 | },
846 | {
847 | "dt": 1588165200,
848 | "temp": 294.76,
849 | "feels_like": 293.76,
850 | "pressure": 1013,
851 | "humidity": 37,
852 | "dew_point": 280.86,
853 | "clouds": 32,
854 | "wind_speed": 0.2,
855 | "wind_deg": 253,
856 | "weather": [
857 | {
858 | "id": 802,
859 | "main": "Clouds",
860 | "description": "scattered clouds",
861 | "icon": "03n"
862 | }
863 | ]
864 | },
865 | {
866 | "dt": 1588168800,
867 | "temp": 295.53,
868 | "feels_like": 294.54,
869 | "pressure": 1014,
870 | "humidity": 37,
871 | "dew_point": 281.56,
872 | "clouds": 66,
873 | "wind_speed": 0.4,
874 | "wind_deg": 193,
875 | "weather": [
876 | {
877 | "id": 803,
878 | "main": "Clouds",
879 | "description": "broken clouds",
880 | "icon": "04d"
881 | }
882 | ]
883 | },
884 | {
885 | "dt": 1588172400,
886 | "temp": 297.4,
887 | "feels_like": 296.15,
888 | "pressure": 1014,
889 | "humidity": 34,
890 | "dew_point": 281.81,
891 | "clouds": 76,
892 | "wind_speed": 0.91,
893 | "wind_deg": 142,
894 | "weather": [
895 | {
896 | "id": 803,
897 | "main": "Clouds",
898 | "description": "broken clouds",
899 | "icon": "04d"
900 | }
901 | ]
902 | },
903 | {
904 | "dt": 1588176000,
905 | "temp": 299.11,
906 | "feels_like": 297.33,
907 | "pressure": 1014,
908 | "humidity": 31,
909 | "dew_point": 281.96,
910 | "clouds": 77,
911 | "wind_speed": 1.71,
912 | "wind_deg": 210,
913 | "weather": [
914 | {
915 | "id": 803,
916 | "main": "Clouds",
917 | "description": "broken clouds",
918 | "icon": "04d"
919 | }
920 | ]
921 | },
922 | {
923 | "dt": 1588179600,
924 | "temp": 300.46,
925 | "feels_like": 298.49,
926 | "pressure": 1014,
927 | "humidity": 29,
928 | "dew_point": 282.19,
929 | "clouds": 75,
930 | "wind_speed": 2.05,
931 | "wind_deg": 208,
932 | "weather": [
933 | {
934 | "id": 803,
935 | "main": "Clouds",
936 | "description": "broken clouds",
937 | "icon": "04d"
938 | }
939 | ]
940 | }
941 | ],
942 | "daily": [
943 | {
944 | "dt": 1588014000,
945 | "sunrise": 1587992793,
946 | "sunset": 1588041202,
947 | "temp": {
948 | "day": 299.04,
949 | "min": 295.41,
950 | "max": 300.74,
951 | "night": 295.41,
952 | "eve": 300.74,
953 | "morn": 299.04
954 | },
955 | "feels_like": {
956 | "day": 299,
957 | "night": 294.52,
958 | "eve": 298.36,
959 | "morn": 299
960 | },
961 | "pressure": 1015,
962 | "humidity": 53,
963 | "dew_point": 288.74,
964 | "wind_speed": 2.66,
965 | "wind_deg": 221,
966 | "weather": [
967 | {
968 | "id": 800,
969 | "main": "Clear",
970 | "description": "clear sky",
971 | "icon": "01d"
972 | }
973 | ],
974 | "clouds": 1,
975 | "uvi": 9.72
976 | },
977 | {
978 | "dt": 1588100400,
979 | "sunrise": 1588079130,
980 | "sunset": 1588127650,
981 | "temp": {
982 | "day": 301.57,
983 | "min": 293.57,
984 | "max": 304.31,
985 | "night": 296.84,
986 | "eve": 303.34,
987 | "morn": 293.57
988 | },
989 | "feels_like": {
990 | "day": 299.16,
991 | "night": 295.94,
992 | "eve": 300.43,
993 | "morn": 292.35
994 | },
995 | "pressure": 1015,
996 | "humidity": 25,
997 | "dew_point": 281.01,
998 | "wind_speed": 2.28,
999 | "wind_deg": 223,
1000 | "weather": [
1001 | {
1002 | "id": 800,
1003 | "main": "Clear",
1004 | "description": "clear sky",
1005 | "icon": "01d"
1006 | }
1007 | ],
1008 | "clouds": 0,
1009 | "uvi": 9.72
1010 | },
1011 | {
1012 | "dt": 1588186800,
1013 | "sunrise": 1588165467,
1014 | "sunset": 1588214098,
1015 | "temp": {
1016 | "day": 301.97,
1017 | "min": 295.01,
1018 | "max": 303.77,
1019 | "night": 295.08,
1020 | "eve": 301.26,
1021 | "morn": 295.01
1022 | },
1023 | "feels_like": {
1024 | "day": 299.52,
1025 | "night": 294.1,
1026 | "eve": 298.13,
1027 | "morn": 294.1
1028 | },
1029 | "pressure": 1014,
1030 | "humidity": 26,
1031 | "dew_point": 281.94,
1032 | "wind_speed": 2.63,
1033 | "wind_deg": 204,
1034 | "weather": [
1035 | {
1036 | "id": 803,
1037 | "main": "Clouds",
1038 | "description": "broken clouds",
1039 | "icon": "04d"
1040 | }
1041 | ],
1042 | "clouds": 67,
1043 | "uvi": 9.26
1044 | },
1045 | {
1046 | "dt": 1588273200,
1047 | "sunrise": 1588251806,
1048 | "sunset": 1588300545,
1049 | "temp": {
1050 | "day": 300.14,
1051 | "min": 293.96,
1052 | "max": 302.83,
1053 | "night": 295.15,
1054 | "eve": 301.88,
1055 | "morn": 293.96
1056 | },
1057 | "feels_like": {
1058 | "day": 298.15,
1059 | "night": 293.08,
1060 | "eve": 298.83,
1061 | "morn": 293.33
1062 | },
1063 | "pressure": 1013,
1064 | "humidity": 30,
1065 | "dew_point": 282.58,
1066 | "wind_speed": 2.15,
1067 | "wind_deg": 251,
1068 | "weather": [
1069 | {
1070 | "id": 804,
1071 | "main": "Clouds",
1072 | "description": "overcast clouds",
1073 | "icon": "04d"
1074 | }
1075 | ],
1076 | "clouds": 100,
1077 | "uvi": 9.1
1078 | },
1079 | {
1080 | "dt": 1588359600,
1081 | "sunrise": 1588338146,
1082 | "sunset": 1588386993,
1083 | "temp": {
1084 | "day": 299.4,
1085 | "min": 292.52,
1086 | "max": 301.62,
1087 | "night": 293.77,
1088 | "eve": 300.44,
1089 | "morn": 292.52
1090 | },
1091 | "feels_like": {
1092 | "day": 296.46,
1093 | "night": 291.51,
1094 | "eve": 296.33,
1095 | "morn": 291.11
1096 | },
1097 | "pressure": 1012,
1098 | "humidity": 25,
1099 | "dew_point": 279.02,
1100 | "wind_speed": 2.49,
1101 | "wind_deg": 227,
1102 | "weather": [
1103 | {
1104 | "id": 803,
1105 | "main": "Clouds",
1106 | "description": "broken clouds",
1107 | "icon": "04d"
1108 | }
1109 | ],
1110 | "clouds": 59,
1111 | "uvi": 9.1
1112 | },
1113 | {
1114 | "dt": 1588446000,
1115 | "sunrise": 1588424486,
1116 | "sunset": 1588473441,
1117 | "temp": {
1118 | "day": 297.05,
1119 | "min": 290.78,
1120 | "max": 299.17,
1121 | "night": 291.49,
1122 | "eve": 297.65,
1123 | "morn": 290.78
1124 | },
1125 | "feels_like": {
1126 | "day": 293.21,
1127 | "night": 289.17,
1128 | "eve": 293.46,
1129 | "morn": 288.87
1130 | },
1131 | "pressure": 1014,
1132 | "humidity": 25,
1133 | "dew_point": 277.16,
1134 | "wind_speed": 3.26,
1135 | "wind_deg": 225,
1136 | "weather": [
1137 | {
1138 | "id": 800,
1139 | "main": "Clear",
1140 | "description": "clear sky",
1141 | "icon": "01d"
1142 | }
1143 | ],
1144 | "clouds": 8,
1145 | "uvi": 9.59
1146 | },
1147 | {
1148 | "dt": 1588532400,
1149 | "sunrise": 1588510828,
1150 | "sunset": 1588559889,
1151 | "temp": {
1152 | "day": 295.83,
1153 | "min": 289.12,
1154 | "max": 298.75,
1155 | "night": 291.11,
1156 | "eve": 298.75,
1157 | "morn": 289.12
1158 | },
1159 | "feels_like": {
1160 | "day": 292.73,
1161 | "night": 285.34,
1162 | "eve": 293.86,
1163 | "morn": 287.98
1164 | },
1165 | "pressure": 1015,
1166 | "humidity": 31,
1167 | "dew_point": 279.14,
1168 | "wind_speed": 2.73,
1169 | "wind_deg": 267,
1170 | "weather": [
1171 | {
1172 | "id": 800,
1173 | "main": "Clear",
1174 | "description": "clear sky",
1175 | "icon": "01d"
1176 | }
1177 | ],
1178 | "clouds": 0,
1179 | "uvi": 9.54
1180 | },
1181 | {
1182 | "dt": 1588618800,
1183 | "sunrise": 1588597171,
1184 | "sunset": 1588646337,
1185 | "temp": {
1186 | "day": 297.61,
1187 | "min": 289.28,
1188 | "max": 300.36,
1189 | "night": 293.13,
1190 | "eve": 299.49,
1191 | "morn": 289.28
1192 | },
1193 | "feels_like": {
1194 | "day": 294.35,
1195 | "night": 289.71,
1196 | "eve": 295.15,
1197 | "morn": 285.31
1198 | },
1199 | "pressure": 1014,
1200 | "humidity": 18,
1201 | "dew_point": 273.03,
1202 | "wind_speed": 1.54,
1203 | "wind_deg": 240,
1204 | "weather": [
1205 | {
1206 | "id": 800,
1207 | "main": "Clear",
1208 | "description": "clear sky",
1209 | "icon": "01d"
1210 | }
1211 | ],
1212 | "clouds": 0,
1213 | "uvi": 9.82
1214 | }
1215 | ]
1216 | }
1217 |
--------------------------------------------------------------------------------
/Weather/Resources/Mocks/MockWeatherFetcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockWeatherFetcher.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | class MockWeatherFetcher: WeatherFetcher {
13 | func weatherSummary(forCity city: String) -> AnyPublisher {
14 | return Just(WeatherSummary.createMock())
15 | .setFailureType(to: WeatherError.self)
16 | .eraseToAnyPublisher()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Weather/Resources/Mocks/MockWeatherSummary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockWeatherSummary.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if DEBUG
12 | extension WeatherSummary {
13 | static func createMock() -> WeatherSummary {
14 | let path = Bundle.main.path(forResource: "MockOneCallResponse", ofType: "json")!
15 | let url = URL(fileURLWithPath: path)
16 | let data = try! Data(contentsOf: url)
17 | let response = try! JSONDecoder().decode(OneCallResponse.self, from: data)
18 | return WeatherSummary.convert(fromResponse: response)
19 | }
20 | }
21 | #endif
22 |
--------------------------------------------------------------------------------
/Weather/Resources/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Weather/Sources/DataManager/DataManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataManager.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreLocation
11 | import Combine
12 |
13 | protocol WeatherFetcher {
14 | func weatherSummary(forCity city: String) -> AnyPublisher
15 | }
16 |
17 | class DataManager {
18 | let session: URLSession
19 |
20 | init(session: URLSession = .init(configuration: .default)) {
21 | self.session = session
22 | }
23 | }
24 |
25 | extension DataManager: WeatherFetcher {
26 | func weatherSummary(forCity city: String) -> AnyPublisher {
27 | getCoordinates(forAddressString: city)
28 | .flatMap { [weak self] coordinates -> AnyPublisher in
29 | guard let self = self,
30 | let url = self.makeWeatherSummaryComponents(withCoordinates: coordinates).url else {
31 | return Fail(error: WeatherError.network(message: "Could not create URL"))
32 | .eraseToAnyPublisher()
33 | }
34 | return self.session.dataTaskPublisher(for: URLRequest(url: url))
35 | .mapError { WeatherError.network(message: $0.localizedDescription) }
36 | .eraseToAnyPublisher()
37 | }.flatMap { pair in
38 | Just(pair.data)
39 | .decode(type: OneCallResponse.self, decoder: JSONDecoder())
40 | .mapError { error in
41 | WeatherError.parsing(message: error.localizedDescription)
42 | }.map { response in
43 | WeatherSummary.convert(fromResponse: response)
44 | }.eraseToAnyPublisher()
45 | }.eraseToAnyPublisher()
46 | }
47 | }
48 |
49 | private extension DataManager {
50 | struct OpenWeatherAPI {
51 | static let scheme: String = "https"
52 | static let host: String = "api.openweathermap.org"
53 | static let path: String = "/data/2.5"
54 | static var key: String? {
55 | if let path = Bundle.main.path(forResource: "Secrets", ofType: "plist"),
56 | let dict = NSDictionary(contentsOfFile: path),
57 | let key = dict["openWeatherAPIKey"] as? String {
58 | return key
59 | }
60 | return nil
61 | }
62 | }
63 |
64 | func makeWeatherSummaryComponents(withCoordinates coordinates: CLLocationCoordinate2D) -> URLComponents {
65 | var components = URLComponents()
66 | components.scheme = OpenWeatherAPI.scheme
67 | components.host = OpenWeatherAPI.host
68 | components.path = OpenWeatherAPI.path + "/onecall"
69 |
70 | components.queryItems = [
71 | .init(name: "lat", value: "\(coordinates.latitude)"),
72 | .init(name: "lon", value: "\(coordinates.longitude)"),
73 | .init(name: "appid", value: OpenWeatherAPI.key)]
74 |
75 | return components
76 | }
77 |
78 | func getCoordinates(forAddressString addressString: String) -> AnyPublisher {
79 | let geocoder = CLGeocoder()
80 | return Future { promise in
81 | geocoder.geocodeAddressString(addressString) { placemarks, error in
82 | if let error = error {
83 | print(error)
84 | promise(.failure(WeatherError.location(message: error.localizedDescription)))
85 | return
86 | }
87 | guard let location = placemarks?.first?.location else {
88 | promise(.failure(WeatherError.location(message: "Could not find location for \(addressString)")))
89 | return
90 | }
91 | promise(.success(location.coordinate))
92 | }
93 | }.eraseToAnyPublisher()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Weather/Sources/Models/OneCallResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OneCallResponse.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 | // This file was generated from JSON Schema using quicktype, do not modify it directly.
9 | // To parse the JSON, add this file to your project and do:
10 | //
11 | // let oneCallResponse = try? newJSONDecoder().decode(OneCallResponse.self, from: jsonData)
12 |
13 | import Foundation
14 |
15 | // MARK: - OneCallResponse
16 | struct OneCallResponse: Codable {
17 | let lat, lon: Double
18 | let timezone: String
19 | let current: CurrentResponse
20 | let hourly: [HourlyResponse]
21 | let daily: [DailyResponse]
22 | }
23 |
24 | // MARK: - CurrentResponse
25 | struct CurrentResponse: Codable {
26 | let dt, sunrise, sunset: Int
27 | let temp, feelsLike: Double
28 | let pressure, humidity: Int
29 | let uvi: Double
30 | let clouds, visibility: Int?
31 | let windSpeed: Double
32 | let windDeg: Int
33 | let weather: [WeatherResponse]
34 | let rain: RainResponse?
35 |
36 | enum CodingKeys: String, CodingKey {
37 | case dt, sunrise, sunset, temp
38 | case feelsLike = "feels_like"
39 | case pressure, humidity, uvi, clouds, visibility
40 | case windSpeed = "wind_speed"
41 | case windDeg = "wind_deg"
42 | case weather, rain
43 | }
44 | }
45 |
46 | // MARK: - RainResponse
47 | struct RainResponse: Codable {
48 | let the1H: Double?
49 |
50 | enum CodingKeys: String, CodingKey {
51 | case the1H = "1h"
52 | }
53 | }
54 |
55 | // MARK: - WeatherResponse
56 | struct WeatherResponse: Codable {
57 | let id: Int
58 | let main, weatherDescription, icon: String
59 |
60 | enum CodingKeys: String, CodingKey {
61 | case id, main
62 | case weatherDescription = "description"
63 | case icon
64 | }
65 | }
66 |
67 | // MARK: - DailyResponse
68 | struct DailyResponse: Codable {
69 | let dt, sunrise, sunset: Int
70 | let temp: TempResponse
71 | let feelsLike: FeelsLikeResponse
72 | let pressure, humidity: Int
73 | let windSpeed: Double
74 | let windDeg: Int
75 | let weather: [WeatherResponse]
76 | let clouds: Int
77 | let rain: Double?
78 | let uvi: Double
79 |
80 | enum CodingKeys: String, CodingKey {
81 | case dt, sunrise, sunset, temp
82 | case feelsLike = "feels_like"
83 | case pressure, humidity
84 | case windSpeed = "wind_speed"
85 | case windDeg = "wind_deg"
86 | case weather, clouds, rain, uvi
87 | }
88 | }
89 |
90 | // MARK: - FeelsLikeResponse
91 | struct FeelsLikeResponse: Codable {
92 | let day, night, eve, morn: Double
93 | }
94 |
95 | // MARK: - TempResponse
96 | struct TempResponse: Codable {
97 | let day, min, max, night: Double
98 | let eve, morn: Double
99 | }
100 |
101 | // MARK: - HourlyResponse
102 | struct HourlyResponse: Codable {
103 | let dt: Int
104 | let temp, feelsLike: Double
105 | let pressure, humidity, clouds: Int
106 | let windSpeed: Double
107 | let windDeg: Int
108 | let weather: [WeatherResponse]
109 | let rain: RainResponse?
110 |
111 | enum CodingKeys: String, CodingKey {
112 | case dt, temp
113 | case feelsLike = "feels_like"
114 | case pressure, humidity, clouds
115 | case windSpeed = "wind_speed"
116 | case windDeg = "wind_deg"
117 | case weather, rain
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Weather/Sources/Models/WeatherError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherError.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum WeatherError: Error {
12 | case parsing(message: String)
13 | case network(message: String)
14 | case location(message: String)
15 |
16 | var localizedDescription: String {
17 | switch self {
18 | case .parsing(let message):
19 | return message
20 | case .network(let message):
21 | return message
22 | case .location(let message):
23 | return message
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Weather/Sources/Models/WeatherSummary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherSummary.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct WeatherSummary {
13 | let latitude, longitude: Double
14 | let timezone: String
15 | let current: CurrentWeatherSummary
16 | let daily: [DailyWeatherSummary]
17 | let hourly: [HourlyWeatherSummary]
18 |
19 | static func convert(fromResponse response: OneCallResponse) -> WeatherSummary {
20 | WeatherSummary(latitude: response.lat,
21 | longitude: response.lon,
22 | timezone: response.timezone,
23 | current: .convert(fromResponse: response.current),
24 | daily: response.daily.map { .convert(fromResponse: $0) },
25 | hourly: response.hourly.map { .convert(fromResponse: $0) })
26 | }
27 | }
28 |
29 | struct CurrentWeatherSummary {
30 | let time, sunriseTime, sunsetTime: Date
31 | let actualTemp, feelsLikeTemp: Temperature
32 | let pressure, humidity: Int
33 | let uvIndex: Double
34 | let windSpeed: Double
35 | let windAngle: Int
36 | var windDirection: String {
37 | if windAngle >= 337 || windAngle < 23 {
38 | return "N"
39 | }
40 | if windAngle < 67 {
41 | return "NE"
42 | }
43 | if windAngle < 113 {
44 | return "E"
45 | }
46 | if windAngle < 157 {
47 | return "SE"
48 | }
49 | if windAngle < 203 {
50 | return "S"
51 | }
52 | if windAngle < 247 {
53 | return "SW"
54 | }
55 | if windAngle < 293 {
56 | return "W"
57 | }
58 | return "NW"
59 | }
60 |
61 | let weatherDetails: [WeatherDetails]
62 |
63 | static func convert(fromResponse response: CurrentResponse) -> CurrentWeatherSummary {
64 | CurrentWeatherSummary(time: Date(timeIntervalSince1970: TimeInterval(response.dt)),
65 | sunriseTime: Date(timeIntervalSince1970: TimeInterval(response.sunrise)),
66 | sunsetTime: Date(timeIntervalSince1970: TimeInterval(response.sunset)),
67 | actualTemp: .init(kelvin: response.temp),
68 | feelsLikeTemp: .init(kelvin: response.feelsLike),
69 | pressure: response.pressure,
70 | humidity: response.humidity,
71 | uvIndex: response.uvi,
72 | windSpeed: response.windSpeed,
73 | windAngle: response.windDeg,
74 | weatherDetails: response.weather.map { .convert(fromResponse: $0) })
75 | }
76 | }
77 |
78 | struct DailyWeatherSummary {
79 | let time, sunriseTime, sunsetTime: Date
80 | let dayTemp, nightTemp: Temperature
81 | let minTemp, maxTemp: Temperature
82 | let eveTemp, mornTemp: Temperature
83 |
84 | let weatherDetails: [WeatherDetails]
85 |
86 | static func convert(fromResponse response: DailyResponse) -> DailyWeatherSummary {
87 | DailyWeatherSummary(time: Date(timeIntervalSince1970: TimeInterval(response.dt)),
88 | sunriseTime: Date(timeIntervalSince1970: TimeInterval(response.sunrise)),
89 | sunsetTime: Date(timeIntervalSince1970: TimeInterval(response.sunset)),
90 | dayTemp: .init(kelvin: response.temp.day),
91 | nightTemp: .init(kelvin: response.temp.night),
92 | minTemp: .init(kelvin: response.temp.min),
93 | maxTemp: .init(kelvin: response.temp.max),
94 | eveTemp: .init(kelvin: response.temp.eve),
95 | mornTemp: .init(kelvin: response.temp.morn),
96 | weatherDetails: response.weather.map { .convert(fromResponse: $0) })
97 | }
98 | }
99 |
100 | struct HourlyWeatherSummary {
101 | let time: Date
102 | let actualTemp, feelsLikeTemp: Temperature
103 |
104 | let weatherDetails: [WeatherDetails]
105 |
106 | static func convert(fromResponse response: HourlyResponse) -> HourlyWeatherSummary {
107 | HourlyWeatherSummary(time: Date(timeIntervalSince1970: TimeInterval(response.dt)),
108 | actualTemp: .init(kelvin: response.temp),
109 | feelsLikeTemp: .init(kelvin: response.feelsLike),
110 | weatherDetails: response.weather.map { .convert(fromResponse: $0) })
111 | }
112 | }
113 |
114 | struct WeatherDetails {
115 | let weatherID: Int
116 | let weatherCondition: String
117 | let weatherDescription: String
118 | let weatherIconID: String
119 | var weatherIcon: Image? {
120 | switch weatherIconID {
121 | case "01d": return Image(systemName: "sun.max")
122 | case "01n": return Image(systemName: "moon")
123 | case "02d": return Image(systemName: "cloud.sun")
124 | case "02n": return Image(systemName: "cloud.moon")
125 | case "03d", "03n", "04d", "04n": return Image(systemName: "cloud")
126 | case "09d", "09n": return Image(systemName: "cloud.rain")
127 | case "10d": return Image(systemName: "cloud.sun.rain")
128 | case "10n": return Image(systemName: "cloud.moon.rain")
129 | case "11d", "11n": return Image(systemName: "cloud.bolt.rain")
130 | case "13d", "13n": return Image(systemName: "cloud.snow")
131 | case "50d", "50n": return Image(systemName: "cloud.fog")
132 | default: return Image(systemName: "sun.max")
133 | }
134 | }
135 |
136 | static func convert(fromResponse response: WeatherResponse) -> WeatherDetails {
137 | WeatherDetails(weatherID: response.id,
138 | weatherCondition: response.main,
139 | weatherDescription: response.weatherDescription,
140 | weatherIconID: response.icon)
141 | }
142 | }
143 |
144 | struct Temperature {
145 | var kelvin: Double
146 |
147 | var celsius: Double {
148 | kelvin - 273.15
149 | }
150 |
151 | var fahrenheight: Double {
152 | (kelvin - 273.15) * (9 / 5) + 32
153 | }
154 | }
155 |
156 | extension Temperature: ExpressibleByFloatLiteral {
157 | init(floatLiteral value: FloatLiteralType) {
158 | kelvin = value
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/Weather/Sources/Utilities/DismissKeyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DismissKeyboard.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIApplication {
12 | func endEditing() {
13 | sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/CurrentSummary/CurrentSummaryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CurrentSummaryView.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct CurrentSummaryView: View {
12 | private let viewModel: CurrentSummaryViewModel
13 |
14 | init(viewModel: CurrentSummaryViewModel) {
15 | self.viewModel = viewModel
16 | }
17 |
18 | var body: some View {
19 | VStack {
20 | ZStack {
21 | HStack {
22 | VStack(alignment: .leading) {
23 | detailView(text: viewModel.sunriseTimeFmt,
24 | image: .init(systemName: "sunrise"),
25 | offset: .init(width: 0, height: -2))
26 |
27 | detailView(text: viewModel.sunsetTimeFmt,
28 | image: .init(systemName: "sunset"),
29 | offset: .init(width: 0, height: -2))
30 | }
31 | Spacer()
32 | }
33 | VStack(alignment: .leading) {
34 | detailView(text: "UV: \(viewModel.uvIndex)",
35 | image: .init(systemName: "sun.max"))
36 |
37 | detailView(text: viewModel.humidity,
38 | image: .init(systemName: "thermometer.sun"))
39 | }
40 |
41 | HStack {
42 | Spacer()
43 | VStack(alignment: .leading) {
44 | detailView(text: viewModel.windSpeed,
45 | image: .init(systemName: "wind"))
46 |
47 | detailView(text: viewModel.windDirection,
48 | image: .init(systemName: "arrow.up.right.circle"))
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | private func detailView(text: String, image: Image, offset: CGSize = .zero) -> some View {
56 | HStack {
57 | image
58 | .imageScale(.medium)
59 | .foregroundColor(.blue)
60 | .offset(offset)
61 | Text(text)
62 | }
63 | }
64 | }
65 |
66 |
67 | struct TodayInformationView_Previews: PreviewProvider {
68 | static var previews: some View {
69 | CurrentSummaryView(viewModel: .init(weatherSummary: WeatherSummary.createMock().current))
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/CurrentSummary/CurrentSummaryViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CurrentSummaryViewModel.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class CurrentSummaryViewModel {
12 | private let currentSummary: CurrentWeatherSummary
13 |
14 | var sunriseTimeFmt: String {
15 | let dateFormatter = DateFormatter()
16 | dateFormatter.timeStyle = .short
17 | return dateFormatter.string(from: currentSummary.sunriseTime)
18 | }
19 |
20 | var sunsetTimeFmt: String {
21 | let dateFormatter = DateFormatter()
22 | dateFormatter.timeStyle = .short
23 | return dateFormatter.string(from: currentSummary.sunsetTime)
24 | }
25 |
26 | var windSpeed: String {
27 | let kmh = currentSummary.windSpeed
28 | let mph = kmh / 1.609
29 | return String(format: "%.0f mi/h", mph)
30 | }
31 |
32 | var windDirection: String {
33 | currentSummary.windDirection
34 | }
35 |
36 | var uvIndex: String {
37 | String(format: "%.0f", currentSummary.uvIndex)
38 | }
39 |
40 | var humidity: String {
41 | "\(currentSummary.humidity)%"
42 | }
43 |
44 | init(weatherSummary: CurrentWeatherSummary) {
45 | self.currentSummary = weatherSummary
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/DaySummary/DaySummaryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DaySummaryView.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct DaySummaryView: View {
12 | private let viewModel: DaySummaryViewModel
13 |
14 | init(viewModel: DaySummaryViewModel) {
15 | self.viewModel = viewModel
16 | }
17 |
18 | var body: some View {
19 | HStack {
20 | Text(viewModel.day)
21 | .fontWeight(.medium)
22 | Spacer()
23 | Text("\(viewModel.highTempFmt) / \(viewModel.lowTempFmt)")
24 | .fontWeight(.light)
25 | viewModel.icon
26 | .imageScale(.large)
27 | .foregroundColor(.secondary)
28 | }.padding(.horizontal)
29 | .padding(.vertical, 22)
30 | .background(Color.init(.secondarySystemBackground))
31 | .cornerRadius(10)
32 | }
33 | }
34 |
35 | struct DaySummaryView_Previews: PreviewProvider {
36 | static var previews: some View {
37 | DaySummaryView(viewModel: DaySummaryViewModel(daySummary: WeatherSummary.createMock().daily.first!))
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/DaySummary/DaySummaryViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DaySummaryViewModel.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | class DaySummaryViewModel: Identifiable {
13 | private let daySummary: DailyWeatherSummary
14 |
15 | var id = UUID()
16 |
17 | var day: String {
18 | let dateFormatter = DateFormatter()
19 | dateFormatter.dateFormat = "EEEE"
20 | return dateFormatter.string(from: daySummary.time)
21 | }
22 |
23 | var highTempFmt: String {
24 | String(format: "%.0fº", daySummary.maxTemp.fahrenheight)
25 | }
26 |
27 | var lowTempFmt: String {
28 | String(format: "%.0fº", daySummary.minTemp.fahrenheight)
29 | }
30 |
31 | var icon: Image? {
32 | daySummary.weatherDetails.first?.weatherIcon
33 | }
34 |
35 | init(daySummary: DailyWeatherSummary) {
36 | self.daySummary = daySummary
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/HourSummary/HourInformationViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HourSummaryViewModel.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class HourSummaryViewModel: Identifiable {
12 | private let hourSummary: HourlyWeatherSummary
13 |
14 | var id = UUID()
15 |
16 | init(hourSummary: HourlyWeatherSummary) {
17 | self.hourSummary = hourSummary
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/HourSummary/HourSummaryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HourSummaryView.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct HourSummaryView: View {
12 | private let viewModel: HourSummaryViewModel
13 |
14 | init(viewModel: HourSummaryViewModel) {
15 | self.viewModel = viewModel
16 | }
17 |
18 | var body: some View {
19 | VStack(spacing: 16) {
20 | Text(viewModel.tempFmt)
21 | .font(.caption)
22 | .fontWeight(.medium)
23 | viewModel.icon
24 | .imageScale(.large)
25 | Text(viewModel.timeFmt)
26 | .font(.caption)
27 | .fontWeight(.medium)
28 | .layoutPriority(1)
29 | .fixedSize()
30 | }.padding(10)
31 | .background(Color.init(.secondarySystemBackground))
32 | .cornerRadius(10)
33 | }
34 | }
35 |
36 | struct HourSummaryView_Previews: PreviewProvider {
37 | static var previews: some View {
38 | HourSummaryView(viewModel: HourSummaryViewModel(hourSummary: WeatherSummary.createMock().hourly.first!))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/HourSummary/HourSummaryViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HourSummaryViewModel.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | class HourSummaryViewModel: Identifiable {
13 | private let hourSummary: HourlyWeatherSummary
14 |
15 | var id = UUID()
16 |
17 | var tempFmt: String {
18 | String(format: "%.0fº", hourSummary.actualTemp.fahrenheight)
19 | }
20 |
21 | var icon: Image? {
22 | hourSummary.weatherDetails.first?.weatherIcon
23 | }
24 |
25 | var timeFmt: String {
26 | let dateFormatter = DateFormatter()
27 | dateFormatter.dateFormat = "h:00 a"
28 | return dateFormatter.string(from: hourSummary.time)
29 | }
30 |
31 | init(hourSummary: HourlyWeatherSummary) {
32 | self.hourSummary = hourSummary
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/SearchBar/SearchBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchBar.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SearchBar: View {
12 | @Binding var searchText: String
13 | @State var showClearButton: Bool = false
14 | var placeholder = "Search"
15 |
16 | var body: some View {
17 | TextField(placeholder, text: $searchText, onEditingChanged: { editing in
18 | self.showClearButton = editing
19 | }, onCommit: {
20 | self.showClearButton = false
21 | }).modifier(ClearButton(text: $searchText, isVisible: $showClearButton))
22 | .padding(.horizontal)
23 | .padding(.vertical, 10)
24 | .background(Color(.secondarySystemBackground))
25 | .cornerRadius(12)
26 | }
27 | }
28 |
29 | struct ClearButton: ViewModifier {
30 | @Binding var text: String
31 | @Binding var isVisible: Bool
32 |
33 | func body(content: Content) -> some View {
34 | HStack {
35 | content
36 | Spacer()
37 | Image(systemName: "xmark.circle.fill")
38 | .foregroundColor(Color.init(.placeholderText))
39 | .opacity(!text.isEmpty ? 1 : 0)
40 | .opacity(isVisible ? 1 : 0)
41 | .onTapGesture { self.text = "" }
42 | }
43 | }
44 | }
45 |
46 | struct SearchBar_Previews: PreviewProvider {
47 | @State static var searchText: String = ""
48 | static var previews: some View {
49 | SearchBar(searchText: $searchText)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/WeatherSummary/WeatherSummaryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherSummaryView.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WeatherSummaryView: View {
12 | @ObservedObject var viewModel: WeatherSummaryViewModel
13 |
14 | init(viewModel: WeatherSummaryViewModel) {
15 | self.viewModel = viewModel
16 | }
17 |
18 | var body: some View {
19 | ZStack {
20 | Color(.systemBackground)
21 | .edgesIgnoringSafeArea(.all)
22 | VStack(spacing: 32) {
23 | SearchBar(searchText: $viewModel.searchText, placeholder: "Search for a location")
24 | currentWeatherView
25 | viewModel.todayInformationViewModel() != nil ?
26 | CurrentSummaryView(viewModel: viewModel.todayInformationViewModel()!) : nil
27 | ScrollView(.horizontal, showsIndicators: false) {
28 | HStack {
29 | ForEach(viewModel.hourSummaries) { viewModel in
30 | HourSummaryView(viewModel: viewModel)
31 | }
32 | }
33 | }
34 | ScrollView(.vertical, showsIndicators: false) {
35 | VStack {
36 | ForEach(viewModel.daySummaries) { viewModel in
37 | DaySummaryView(viewModel: viewModel)
38 | }
39 | }
40 | }
41 | }.padding([.horizontal])
42 | .padding(.top)
43 | }.onTapGesture {
44 | UIApplication.shared.endEditing()
45 | }
46 | }
47 |
48 | private var currentWeatherView: some View {
49 | if let _ = viewModel.weatherSummary {
50 | return AnyView(HStack {
51 | VStack(spacing: 4) {
52 | Text(viewModel.currentTempDescription)
53 | .font(.headline)
54 | .fontWeight(.medium)
55 | HStack {
56 | viewModel.currentWeatherIcon
57 | .imageScale(.small)
58 | Text("\(viewModel.currentTempFmt)")
59 | .fontWeight(.semibold)
60 | }.font(.system(size: 64))
61 | .frame(maxWidth: .infinity)
62 | HStack(spacing: 16) {
63 | Text("Feels like \(viewModel.feelsLikeTempFmt)")
64 | .foregroundColor(.secondary)
65 | }
66 | }
67 | })
68 | } else {
69 | return AnyView(EmptyView())
70 | }
71 | }
72 | }
73 |
74 | struct WeatherSummaryView_Previews: PreviewProvider {
75 | static var previews: some View {
76 | WeatherSummaryView(viewModel: WeatherSummaryViewModel(weatherFetcher: MockWeatherFetcher()))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Weather/Sources/Views/WeatherSummary/WeatherSummaryViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherSummaryViewModel.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Combine
11 |
12 | class WeatherSummaryViewModel: ObservableObject {
13 | private var weatherFetcher: WeatherFetcher
14 | private var disposable = Set()
15 |
16 | @Published var searchText: String = "Cupertino"
17 | @Published var weatherSummary: WeatherSummary?
18 |
19 | var currentTempFmt: String {
20 | guard let temp = weatherSummary?.current.actualTemp.fahrenheight else { return "--º" }
21 | return String(format: "%.0fº", temp)
22 | }
23 |
24 | var feelsLikeTempFmt: String {
25 | guard let temp = weatherSummary?.current.feelsLikeTemp.fahrenheight else { return "--º" }
26 | return String(format: "%.0fº", temp)
27 | }
28 |
29 | var currentTempDescription: String {
30 | guard let description = weatherSummary?.current.weatherDetails.first?.weatherDescription else {
31 | return ""
32 | }
33 | return description.localizedCapitalized
34 | }
35 |
36 | var currentWeatherIcon: Image? {
37 | weatherSummary?.current.weatherDetails.first?.weatherIcon
38 | }
39 |
40 | var hourSummaries: [HourSummaryViewModel] = []
41 | var daySummaries: [DaySummaryViewModel] = []
42 |
43 | init(weatherFetcher: WeatherFetcher) {
44 | self.weatherFetcher = weatherFetcher
45 |
46 | let searchTextScheduler: DispatchQueue = .init(label: "weatherSearch", qos: .userInteractive)
47 | $searchText
48 | .dropFirst()
49 | .debounce(for: .seconds(0.5),
50 | scheduler: searchTextScheduler)
51 | .sink(receiveValue: fetchWeatherSummary(forLocation:))
52 | .store(in: &disposable)
53 |
54 | fetchWeatherSummary(forLocation: searchText)
55 | }
56 |
57 | func fetchWeatherSummary(forLocation location: String) {
58 | weatherFetcher.weatherSummary(forCity: location)
59 | .receive(on: DispatchQueue.main)
60 | .sink(receiveCompletion: { [weak self] (value) in
61 | switch value {
62 | case .failure(let error):
63 | self?.weatherSummary = nil
64 | self?.hourSummaries = []
65 | self?.daySummaries = []
66 | print(error)
67 | case .finished:
68 | break
69 | }
70 | }) { [weak self] weatherSummary in
71 | self?.weatherSummary = weatherSummary
72 | self?.hourSummaries = weatherSummary.hourly.map { HourSummaryViewModel(hourSummary: $0) }
73 | self?.daySummaries = weatherSummary.daily.map { DaySummaryViewModel(daySummary: $0) }
74 | }.store(in: &disposable)
75 | }
76 |
77 | func todayInformationViewModel() -> CurrentSummaryViewModel? {
78 | guard let weatherSummary = weatherSummary else { return nil }
79 | return .init(weatherSummary: weatherSummary.current)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Weather/Support/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/Weather/Support/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Weather/Support/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Weather
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
21 |
22 | // Create the SwiftUI view that provides the window contents.
23 | let dataManager = DataManager()
24 | let viewModel = WeatherSummaryViewModel(weatherFetcher: dataManager)
25 | let contentView = WeatherSummaryView(viewModel: viewModel)
26 |
27 | // Use a UIHostingController as window root view controller.
28 | if let windowScene = scene as? UIWindowScene {
29 | let window = UIWindow(windowScene: windowScene)
30 | window.rootViewController = UIHostingController(rootView: contentView)
31 | self.window = window
32 | window.makeKeyAndVisible()
33 | }
34 | }
35 |
36 | func sceneDidDisconnect(_ scene: UIScene) {
37 | // Called as the scene is being released by the system.
38 | // This occurs shortly after the scene enters the background, or when its session is discarded.
39 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
40 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
41 | }
42 |
43 | func sceneDidBecomeActive(_ scene: UIScene) {
44 | // Called when the scene has moved from an inactive state to an active state.
45 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
46 | }
47 |
48 | func sceneWillResignActive(_ scene: UIScene) {
49 | // Called when the scene will move from an active state to an inactive state.
50 | // This may occur due to temporary interruptions (ex. an incoming phone call).
51 | }
52 |
53 | func sceneWillEnterForeground(_ scene: UIScene) {
54 | // Called as the scene transitions from the background to the foreground.
55 | // Use this method to undo the changes made on entering the background.
56 | }
57 |
58 | func sceneDidEnterBackground(_ scene: UIScene) {
59 | // Called as the scene transitions from the foreground to the background.
60 | // Use this method to save data, release shared resources, and store enough scene-specific state information
61 | // to restore the scene back to its current state.
62 | }
63 |
64 |
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/WeatherTests/CurrentSummaryViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CurrentSummaryViewModelTests.swift
3 | // WeatherTests
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import SwiftUI
11 | @testable import Weather
12 |
13 | class CurrentSummaryViewModelTests: XCTestCase {
14 | var viewModel: CurrentSummaryViewModel!
15 |
16 | override func setUp() {
17 | viewModel = CurrentSummaryViewModel(weatherSummary: WeatherSummary.createMock().current)
18 | }
19 |
20 | func test_currentSummaryViewModel() {
21 | XCTAssertEqual(viewModel.sunriseTimeFmt, "6:06 AM")
22 | XCTAssertEqual(viewModel.sunsetTimeFmt, "7:33 PM")
23 | XCTAssertEqual(viewModel.windSpeed, "1 mi/h")
24 | XCTAssertEqual(viewModel.windDirection, "S")
25 | XCTAssertEqual(viewModel.uvIndex, "10")
26 | XCTAssertEqual(viewModel.humidity, "53%")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/WeatherTests/DaySummaryViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DaySummaryViewModelTests.swift
3 | // WeatherTests
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import SwiftUI
11 | @testable import Weather
12 |
13 | class DaySummaryViewModelTests: XCTestCase {
14 | var viewModel: DaySummaryViewModel!
15 |
16 | override func setUp() {
17 | viewModel = DaySummaryViewModel(daySummary: WeatherSummary.createMock().daily.first!)
18 | }
19 |
20 | func test_daySummaryViewModel() {
21 | XCTAssertEqual(viewModel.day, "Monday")
22 | XCTAssertEqual(viewModel.lowTempFmt, "72º")
23 | XCTAssertEqual(viewModel.highTempFmt, "82º")
24 | XCTAssertEqual(viewModel.icon, Image(systemName: "sun.max"))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/WeatherTests/HourSummaryViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HourSummaryViewModelTests.swift
3 | // WeatherTests
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import SwiftUI
11 | @testable import Weather
12 |
13 | class HourSummaryViewModelTests: XCTestCase {
14 | var viewModel: HourSummaryViewModel!
15 |
16 | override func setUp() {
17 | self.viewModel = HourSummaryViewModel(hourSummary: WeatherSummary.createMock().hourly.first!)
18 | }
19 |
20 | func test_hourSummaryViewModel() {
21 | XCTAssertEqual(viewModel.tempFmt, "79º")
22 | XCTAssertEqual(viewModel.timeFmt, "11:00 AM")
23 | XCTAssertEqual(viewModel.icon, Image(systemName: "sun.max"))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/WeatherTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/WeatherTests/WeatherSummaryViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherSummaryViewModelTests.swift
3 | // WeatherTests
4 | //
5 | // Created by Alex Fargo on 4/27/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import SwiftUI
11 | @testable import Weather
12 |
13 | class WeatherSummaryViewModelTests: XCTestCase {
14 | var viewModel: WeatherSummaryViewModel!
15 |
16 | override func setUp() {
17 | viewModel = WeatherSummaryViewModel(weatherFetcher: MockWeatherFetcher())
18 | }
19 |
20 | func test_WeatherSummaryViewModel() {
21 | viewModel.searchText = "test"
22 | RunLoop.main.run(mode: .default, before: .distantPast)
23 | XCTAssertEqual(viewModel.currentTempFmt, "79º")
24 | XCTAssertEqual(viewModel.feelsLikeTempFmt, "79º")
25 | XCTAssertEqual(viewModel.currentTempDescription, "Clear Sky")
26 | XCTAssertEqual(viewModel.currentWeatherIcon, Image(systemName: "sun.max"))
27 |
28 | XCTAssertEqual(viewModel.hourSummaries.count, 48)
29 | XCTAssertEqual(viewModel.daySummaries.count, 8)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/WeatherTests/WeatherTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherTests.swift
3 | // WeatherTests
4 | //
5 | // Created by Alex Fargo on 4/26/20.
6 | // Copyright © 2020 Alex Fargo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Weather
11 |
12 | class WeatherTests: XCTestCase {
13 |
14 | override func setUpWithError() throws {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() throws {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() throws {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/screenshots/screencap1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flexaargo/SwiftUI-Weather/3a1df8c8270efbd7a3f170afb99e1f23c16c7983/screenshots/screencap1.gif
--------------------------------------------------------------------------------
/screenshots/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flexaargo/SwiftUI-Weather/3a1df8c8270efbd7a3f170afb99e1f23c16c7983/screenshots/screenshot1.png
--------------------------------------------------------------------------------