├── .gitignore ├── Cloudy.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Cloudy ├── Application Delegate │ └── AppDelegate.swift ├── Configuration │ └── Configuration.swift ├── Extensions │ ├── Conversions.swift │ └── UserDefaults.swift ├── Managers │ └── DataManager.swift ├── Models │ ├── WeatherData.swift │ └── WeatherDayData.swift ├── Protocols │ └── JSONDecodable.swift ├── Resources │ └── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-Small@2x.png │ │ ├── Icon-Small@3x.png │ │ ├── NotificationIcon@2x.png │ │ └── NotificationIcon@3x.png │ │ ├── Contents.json │ │ ├── LaunchScreenIcon.imageset │ │ ├── Contents.json │ │ ├── Icon-60.png │ │ ├── Icon-60@2x.png │ │ └── Icon-60@3x.png │ │ ├── button-settings-normal.imageset │ │ ├── 740-gear.png │ │ ├── 740-gear@2x.png │ │ ├── 740-gear@3x.png │ │ └── Contents.json │ │ ├── clear-day.imageset │ │ ├── Contents.json │ │ ├── clear-day.png │ │ └── clear-day@2x.png │ │ ├── clear-night.imageset │ │ ├── Contents.json │ │ ├── clear-night.png │ │ └── clear-night@2x.png │ │ ├── cloudy.imageset │ │ ├── Contents.json │ │ ├── cloudy.png │ │ └── cloudy@2x.png │ │ ├── fog.imageset │ │ ├── Contents.json │ │ ├── fog.png │ │ └── fog@2x.png │ │ ├── rain.imageset │ │ ├── Contents.json │ │ ├── rain.png │ │ └── rain@2x.png │ │ ├── sleet.imageset │ │ ├── Contents.json │ │ ├── sleet.png │ │ └── sleet@2x.png │ │ └── snow.imageset │ │ ├── Contents.json │ │ ├── snow.png │ │ └── snow@2x.png ├── Storyboards │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Supporting Files │ └── Info.plist └── View Controllers │ ├── Root View Controller │ └── RootViewController.swift │ ├── SettingsViewController │ ├── SettingsViewController.swift │ └── Table View Cells │ │ └── SettingsTableViewCell.swift │ └── Weather View Controllers │ ├── DayViewController.swift │ ├── Table View Cells │ └── WeatherDayTableViewCell.swift │ ├── WeatherViewController.swift │ └── WeekViewController.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ######################### 2 | # .gitignore file for Xcode4 / OS X Source projects 3 | # 4 | # NB: if you are storing "built" products, this WILL NOT WORK, 5 | # and you should use a different .gitignore (or none at all) 6 | # This file is for SOURCE projects, where there are many extra 7 | # files that we want to exclude 8 | # 9 | # For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 10 | ######################### 11 | 12 | ##### 13 | # OS X temporary files that should never be committed 14 | 15 | .DS_Store 16 | *.swp 17 | *.lock 18 | profile 19 | 20 | 21 | #### 22 | # Xcode temporary files that should never be committed 23 | # 24 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 25 | 26 | *~.nib 27 | 28 | 29 | #### 30 | # Xcode build files - 31 | # 32 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 33 | 34 | DerivedData/ 35 | 36 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 37 | 38 | build/ 39 | 40 | 41 | ##### 42 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 43 | # 44 | # This is complicated: 45 | # 46 | # SOMETIMES you need to put this file in version control. 47 | # Apple designed it poorly - if you use "custom executables", they are 48 | # saved in this file. 49 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 50 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 51 | 52 | *.pbxuser 53 | *.mode1v3 54 | *.mode2v3 55 | *.perspectivev3 56 | # NB: also, whitelist the default ones, some projects need to use these 57 | !default.pbxuser 58 | !default.mode1v3 59 | !default.mode2v3 60 | !default.perspectivev3 61 | 62 | 63 | #### 64 | # Xcode 4 - semi-personal settings, often included in workspaces 65 | # 66 | # You can safely ignore the xcuserdata files - but do NOT ignore the files next to them 67 | # 68 | 69 | xcuserdata 70 | 71 | #### 72 | # XCode 4 workspaces - more detailed 73 | # 74 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 75 | # 76 | # Workspace layout is quite spammy. For reference: 77 | # 78 | # (root)/ 79 | # (project-name).xcodeproj/ 80 | # project.pbxproj 81 | # project.xcworkspace/ 82 | # contents.xcworkspacedata 83 | # xcuserdata/ 84 | # (your name)/xcuserdatad/ 85 | # xcuserdata/ 86 | # (your name)/xcuserdatad/ 87 | # 88 | # 89 | # 90 | # Xcode 4 workspaces - SHARED 91 | # 92 | # This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results 93 | # But if you're going to kill personal workspaces, at least keep the shared ones... 94 | # 95 | # 96 | !xcshareddata 97 | 98 | #### 99 | # XCode 4 build-schemes 100 | # 101 | # PRIVATE ones are stored inside xcuserdata 102 | !xcschemes 103 | 104 | #### 105 | # Xcode 4 - Deprecated classes 106 | # 107 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 108 | # 109 | # We're using source-control, so this is a "feature" that we do not want! 110 | 111 | *.moved-aside 112 | 113 | # CocoaPods 114 | /Pods 115 | -------------------------------------------------------------------------------- /Cloudy.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CC08B9DB1DA206EE00754A85 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC08B9DA1DA206EE00754A85 /* SettingsViewController.swift */; }; 11 | CC08B9DE1DA20DB400754A85 /* SettingsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC08B9DD1DA20DB400754A85 /* SettingsTableViewCell.swift */; }; 12 | CC08B9E11DA2169400754A85 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC08B9E01DA2169400754A85 /* UserDefaults.swift */; }; 13 | CC08B9E31DA2208800754A85 /* Conversions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC08B9E21DA2208800754A85 /* Conversions.swift */; }; 14 | CC63745E1DA0F557002E68DC /* WeatherDayTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC63745D1DA0F557002E68DC /* WeatherDayTableViewCell.swift */; }; 15 | CC7854CD1D9FF99700E7836D /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC7854CC1D9FF99700E7836D /* WeatherViewController.swift */; }; 16 | CC89B4381D9FE3D0006BFD6E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B42D1D9FE3D0006BFD6E /* AppDelegate.swift */; }; 17 | CC89B4391D9FE3D0006BFD6E /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B4301D9FE3D0006BFD6E /* RootViewController.swift */; }; 18 | CC89B43B1D9FE3D0006BFD6E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC89B4341D9FE3D0006BFD6E /* Assets.xcassets */; }; 19 | CC89B43C1D9FE3D0006BFD6E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC89B4361D9FE3D0006BFD6E /* LaunchScreen.storyboard */; }; 20 | CC89B43D1D9FE3D0006BFD6E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC89B4371D9FE3D0006BFD6E /* Main.storyboard */; }; 21 | CC89B4401D9FE5BC006BFD6E /* DayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B43F1D9FE5BC006BFD6E /* DayViewController.swift */; }; 22 | CC89B4421D9FE5C6006BFD6E /* WeekViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B4411D9FE5C6006BFD6E /* WeekViewController.swift */; }; 23 | CC89B4451D9FE897006BFD6E /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B4441D9FE897006BFD6E /* DataManager.swift */; }; 24 | CC89B4481D9FE8D3006BFD6E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B4471D9FE8D3006BFD6E /* Configuration.swift */; }; 25 | CC89B44C1D9FEB73006BFD6E /* JSONDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B44B1D9FEB73006BFD6E /* JSONDecodable.swift */; }; 26 | CC89B4501D9FEC2E006BFD6E /* WeatherData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B44F1D9FEC2E006BFD6E /* WeatherData.swift */; }; 27 | CC89B4521D9FECBE006BFD6E /* WeatherDayData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89B4511D9FECBE006BFD6E /* WeatherDayData.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | CC08B9DA1DA206EE00754A85 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 32 | CC08B9DD1DA20DB400754A85 /* SettingsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewCell.swift; sourceTree = ""; }; 33 | CC08B9E01DA2169400754A85 /* UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; 34 | CC08B9E21DA2208800754A85 /* Conversions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Conversions.swift; sourceTree = ""; }; 35 | CC63745D1DA0F557002E68DC /* WeatherDayTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherDayTableViewCell.swift; sourceTree = ""; }; 36 | CC7854CC1D9FF99700E7836D /* WeatherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; sourceTree = ""; }; 37 | CC89B4171D9FE314006BFD6E /* Cloudy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cloudy.app; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | CC89B42D1D9FE3D0006BFD6E /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 39 | CC89B4301D9FE3D0006BFD6E /* RootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 40 | CC89B4321D9FE3D0006BFD6E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | CC89B4341D9FE3D0006BFD6E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 42 | CC89B4361D9FE3D0006BFD6E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 43 | CC89B4371D9FE3D0006BFD6E /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 44 | CC89B43F1D9FE5BC006BFD6E /* DayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DayViewController.swift; sourceTree = ""; }; 45 | CC89B4411D9FE5C6006BFD6E /* WeekViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeekViewController.swift; sourceTree = ""; }; 46 | CC89B4441D9FE897006BFD6E /* DataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; 47 | CC89B4471D9FE8D3006BFD6E /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 48 | CC89B44B1D9FEB73006BFD6E /* JSONDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONDecodable.swift; sourceTree = ""; }; 49 | CC89B44F1D9FEC2E006BFD6E /* WeatherData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherData.swift; sourceTree = ""; }; 50 | CC89B4511D9FECBE006BFD6E /* WeatherDayData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherDayData.swift; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | CC89B4141D9FE314006BFD6E /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | CC08B9D91DA206C300754A85 /* SettingsViewController */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | CC08B9DA1DA206EE00754A85 /* SettingsViewController.swift */, 68 | CC08B9DC1DA20D8D00754A85 /* Table View Cells */, 69 | ); 70 | path = SettingsViewController; 71 | sourceTree = ""; 72 | }; 73 | CC08B9DC1DA20D8D00754A85 /* Table View Cells */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | CC08B9DD1DA20DB400754A85 /* SettingsTableViewCell.swift */, 77 | ); 78 | path = "Table View Cells"; 79 | sourceTree = ""; 80 | }; 81 | CC08B9DF1DA2166B00754A85 /* Extensions */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | CC08B9E21DA2208800754A85 /* Conversions.swift */, 85 | CC08B9E01DA2169400754A85 /* UserDefaults.swift */, 86 | ); 87 | path = Extensions; 88 | sourceTree = ""; 89 | }; 90 | CC63745C1DA0F535002E68DC /* Table View Cells */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | CC63745D1DA0F557002E68DC /* WeatherDayTableViewCell.swift */, 94 | ); 95 | path = "Table View Cells"; 96 | sourceTree = ""; 97 | }; 98 | CC89B40E1D9FE314006BFD6E = { 99 | isa = PBXGroup; 100 | children = ( 101 | CC89B4191D9FE314006BFD6E /* Cloudy */, 102 | CC89B4181D9FE314006BFD6E /* Products */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | CC89B4181D9FE314006BFD6E /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | CC89B4171D9FE314006BFD6E /* Cloudy.app */, 110 | ); 111 | name = Products; 112 | sourceTree = ""; 113 | }; 114 | CC89B4191D9FE314006BFD6E /* Cloudy */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | CC89B42C1D9FE3D0006BFD6E /* Application Delegate */, 118 | CC89B42E1D9FE3D0006BFD6E /* View Controllers */, 119 | CC89B4461D9FE8B6006BFD6E /* Configuration */, 120 | CC89B4351D9FE3D0006BFD6E /* Storyboards */, 121 | CC08B9DF1DA2166B00754A85 /* Extensions */, 122 | CC89B4331D9FE3D0006BFD6E /* Resources */, 123 | CC89B4431D9FE877006BFD6E /* Managers */, 124 | CC89B4491D9FEB36006BFD6E /* Protocols */, 125 | CC89B44E1D9FEC04006BFD6E /* Models */, 126 | CC89B4311D9FE3D0006BFD6E /* Supporting Files */, 127 | ); 128 | path = Cloudy; 129 | sourceTree = ""; 130 | }; 131 | CC89B42C1D9FE3D0006BFD6E /* Application Delegate */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | CC89B42D1D9FE3D0006BFD6E /* AppDelegate.swift */, 135 | ); 136 | path = "Application Delegate"; 137 | sourceTree = ""; 138 | }; 139 | CC89B42E1D9FE3D0006BFD6E /* View Controllers */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | CC89B42F1D9FE3D0006BFD6E /* Root View Controller */, 143 | CC08B9D91DA206C300754A85 /* SettingsViewController */, 144 | CC89B43E1D9FE58C006BFD6E /* Weather View Controllers */, 145 | ); 146 | path = "View Controllers"; 147 | sourceTree = ""; 148 | }; 149 | CC89B42F1D9FE3D0006BFD6E /* Root View Controller */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | CC89B4301D9FE3D0006BFD6E /* RootViewController.swift */, 153 | ); 154 | path = "Root View Controller"; 155 | sourceTree = ""; 156 | }; 157 | CC89B4311D9FE3D0006BFD6E /* Supporting Files */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | CC89B4321D9FE3D0006BFD6E /* Info.plist */, 161 | ); 162 | path = "Supporting Files"; 163 | sourceTree = ""; 164 | }; 165 | CC89B4331D9FE3D0006BFD6E /* Resources */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | CC89B4341D9FE3D0006BFD6E /* Assets.xcassets */, 169 | ); 170 | path = Resources; 171 | sourceTree = ""; 172 | }; 173 | CC89B4351D9FE3D0006BFD6E /* Storyboards */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | CC89B4371D9FE3D0006BFD6E /* Main.storyboard */, 177 | CC89B4361D9FE3D0006BFD6E /* LaunchScreen.storyboard */, 178 | ); 179 | path = Storyboards; 180 | sourceTree = ""; 181 | }; 182 | CC89B43E1D9FE58C006BFD6E /* Weather View Controllers */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | CC89B43F1D9FE5BC006BFD6E /* DayViewController.swift */, 186 | CC89B4411D9FE5C6006BFD6E /* WeekViewController.swift */, 187 | CC7854CC1D9FF99700E7836D /* WeatherViewController.swift */, 188 | CC63745C1DA0F535002E68DC /* Table View Cells */, 189 | ); 190 | path = "Weather View Controllers"; 191 | sourceTree = ""; 192 | }; 193 | CC89B4431D9FE877006BFD6E /* Managers */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | CC89B4441D9FE897006BFD6E /* DataManager.swift */, 197 | ); 198 | path = Managers; 199 | sourceTree = ""; 200 | }; 201 | CC89B4461D9FE8B6006BFD6E /* Configuration */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | CC89B4471D9FE8D3006BFD6E /* Configuration.swift */, 205 | ); 206 | path = Configuration; 207 | sourceTree = ""; 208 | }; 209 | CC89B4491D9FEB36006BFD6E /* Protocols */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | CC89B44B1D9FEB73006BFD6E /* JSONDecodable.swift */, 213 | ); 214 | path = Protocols; 215 | sourceTree = ""; 216 | }; 217 | CC89B44E1D9FEC04006BFD6E /* Models */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | CC89B44F1D9FEC2E006BFD6E /* WeatherData.swift */, 221 | CC89B4511D9FECBE006BFD6E /* WeatherDayData.swift */, 222 | ); 223 | path = Models; 224 | sourceTree = ""; 225 | }; 226 | /* End PBXGroup section */ 227 | 228 | /* Begin PBXNativeTarget section */ 229 | CC89B4161D9FE314006BFD6E /* Cloudy */ = { 230 | isa = PBXNativeTarget; 231 | buildConfigurationList = CC89B4291D9FE314006BFD6E /* Build configuration list for PBXNativeTarget "Cloudy" */; 232 | buildPhases = ( 233 | CC89B4131D9FE314006BFD6E /* Sources */, 234 | CC89B4141D9FE314006BFD6E /* Frameworks */, 235 | CC89B4151D9FE314006BFD6E /* Resources */, 236 | ); 237 | buildRules = ( 238 | ); 239 | dependencies = ( 240 | ); 241 | name = Cloudy; 242 | productName = Cloudy; 243 | productReference = CC89B4171D9FE314006BFD6E /* Cloudy.app */; 244 | productType = "com.apple.product-type.application"; 245 | }; 246 | /* End PBXNativeTarget section */ 247 | 248 | /* Begin PBXProject section */ 249 | CC89B40F1D9FE314006BFD6E /* Project object */ = { 250 | isa = PBXProject; 251 | attributes = { 252 | LastSwiftUpdateCheck = 0800; 253 | LastUpgradeCheck = 0830; 254 | ORGANIZATIONNAME = Cocoacasts; 255 | TargetAttributes = { 256 | CC89B4161D9FE314006BFD6E = { 257 | CreatedOnToolsVersion = 8.0; 258 | DevelopmentTeam = 2493UGBPKJ; 259 | ProvisioningStyle = Automatic; 260 | }; 261 | }; 262 | }; 263 | buildConfigurationList = CC89B4121D9FE314006BFD6E /* Build configuration list for PBXProject "Cloudy" */; 264 | compatibilityVersion = "Xcode 3.2"; 265 | developmentRegion = English; 266 | hasScannedForEncodings = 0; 267 | knownRegions = ( 268 | en, 269 | Base, 270 | ); 271 | mainGroup = CC89B40E1D9FE314006BFD6E; 272 | productRefGroup = CC89B4181D9FE314006BFD6E /* Products */; 273 | projectDirPath = ""; 274 | projectRoot = ""; 275 | targets = ( 276 | CC89B4161D9FE314006BFD6E /* Cloudy */, 277 | ); 278 | }; 279 | /* End PBXProject section */ 280 | 281 | /* Begin PBXResourcesBuildPhase section */ 282 | CC89B4151D9FE314006BFD6E /* Resources */ = { 283 | isa = PBXResourcesBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | CC89B43C1D9FE3D0006BFD6E /* LaunchScreen.storyboard in Resources */, 287 | CC89B43B1D9FE3D0006BFD6E /* Assets.xcassets in Resources */, 288 | CC89B43D1D9FE3D0006BFD6E /* Main.storyboard in Resources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | /* End PBXResourcesBuildPhase section */ 293 | 294 | /* Begin PBXSourcesBuildPhase section */ 295 | CC89B4131D9FE314006BFD6E /* Sources */ = { 296 | isa = PBXSourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | CC08B9E31DA2208800754A85 /* Conversions.swift in Sources */, 300 | CC89B4481D9FE8D3006BFD6E /* Configuration.swift in Sources */, 301 | CC7854CD1D9FF99700E7836D /* WeatherViewController.swift in Sources */, 302 | CC89B4391D9FE3D0006BFD6E /* RootViewController.swift in Sources */, 303 | CC89B44C1D9FEB73006BFD6E /* JSONDecodable.swift in Sources */, 304 | CC89B4451D9FE897006BFD6E /* DataManager.swift in Sources */, 305 | CC08B9DE1DA20DB400754A85 /* SettingsTableViewCell.swift in Sources */, 306 | CC89B4381D9FE3D0006BFD6E /* AppDelegate.swift in Sources */, 307 | CC89B4521D9FECBE006BFD6E /* WeatherDayData.swift in Sources */, 308 | CC89B4421D9FE5C6006BFD6E /* WeekViewController.swift in Sources */, 309 | CC89B4501D9FEC2E006BFD6E /* WeatherData.swift in Sources */, 310 | CC63745E1DA0F557002E68DC /* WeatherDayTableViewCell.swift in Sources */, 311 | CC89B4401D9FE5BC006BFD6E /* DayViewController.swift in Sources */, 312 | CC08B9E11DA2169400754A85 /* UserDefaults.swift in Sources */, 313 | CC08B9DB1DA206EE00754A85 /* SettingsViewController.swift in Sources */, 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | /* End PBXSourcesBuildPhase section */ 318 | 319 | /* Begin XCBuildConfiguration section */ 320 | CC89B4271D9FE314006BFD6E /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ALWAYS_SEARCH_USER_PATHS = NO; 324 | CLANG_ANALYZER_NONNULL = YES; 325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 326 | CLANG_CXX_LIBRARY = "libc++"; 327 | CLANG_ENABLE_MODULES = YES; 328 | CLANG_ENABLE_OBJC_ARC = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_CONSTANT_CONVERSION = YES; 331 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 332 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 333 | CLANG_WARN_EMPTY_BODY = YES; 334 | CLANG_WARN_ENUM_CONVERSION = YES; 335 | CLANG_WARN_INFINITE_RECURSION = YES; 336 | CLANG_WARN_INT_CONVERSION = YES; 337 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 338 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 339 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 340 | CLANG_WARN_UNREACHABLE_CODE = YES; 341 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 342 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 343 | COPY_PHASE_STRIP = NO; 344 | DEBUG_INFORMATION_FORMAT = dwarf; 345 | ENABLE_STRICT_OBJC_MSGSEND = YES; 346 | ENABLE_TESTABILITY = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu99; 348 | GCC_DYNAMIC_NO_PIC = NO; 349 | GCC_NO_COMMON_BLOCKS = YES; 350 | GCC_OPTIMIZATION_LEVEL = 0; 351 | GCC_PREPROCESSOR_DEFINITIONS = ( 352 | "DEBUG=1", 353 | "$(inherited)", 354 | ); 355 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 356 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 357 | GCC_WARN_UNDECLARED_SELECTOR = YES; 358 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 359 | GCC_WARN_UNUSED_FUNCTION = YES; 360 | GCC_WARN_UNUSED_VARIABLE = YES; 361 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 362 | MTL_ENABLE_DEBUG_INFO = YES; 363 | ONLY_ACTIVE_ARCH = YES; 364 | SDKROOT = iphoneos; 365 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 366 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 367 | }; 368 | name = Debug; 369 | }; 370 | CC89B4281D9FE314006BFD6E /* Release */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ALWAYS_SEARCH_USER_PATHS = NO; 374 | CLANG_ANALYZER_NONNULL = YES; 375 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 376 | CLANG_CXX_LIBRARY = "libc++"; 377 | CLANG_ENABLE_MODULES = YES; 378 | CLANG_ENABLE_OBJC_ARC = YES; 379 | CLANG_WARN_BOOL_CONVERSION = YES; 380 | CLANG_WARN_CONSTANT_CONVERSION = YES; 381 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 382 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 383 | CLANG_WARN_EMPTY_BODY = YES; 384 | CLANG_WARN_ENUM_CONVERSION = YES; 385 | CLANG_WARN_INFINITE_RECURSION = YES; 386 | CLANG_WARN_INT_CONVERSION = YES; 387 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu99; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | SDKROOT = iphoneos; 408 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 409 | VALIDATE_PRODUCT = YES; 410 | }; 411 | name = Release; 412 | }; 413 | CC89B42A1D9FE314006BFD6E /* Debug */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 417 | DEVELOPMENT_TEAM = 2493UGBPKJ; 418 | INFOPLIST_FILE = "Cloudy/Supporting Files/Info.plist"; 419 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 420 | PRODUCT_BUNDLE_IDENTIFIER = com.cocoacasts.Cloudy; 421 | PRODUCT_NAME = "$(TARGET_NAME)"; 422 | SWIFT_VERSION = 3.0; 423 | }; 424 | name = Debug; 425 | }; 426 | CC89B42B1D9FE314006BFD6E /* Release */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 430 | DEVELOPMENT_TEAM = 2493UGBPKJ; 431 | INFOPLIST_FILE = "Cloudy/Supporting Files/Info.plist"; 432 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 433 | PRODUCT_BUNDLE_IDENTIFIER = com.cocoacasts.Cloudy; 434 | PRODUCT_NAME = "$(TARGET_NAME)"; 435 | SWIFT_VERSION = 3.0; 436 | }; 437 | name = Release; 438 | }; 439 | /* End XCBuildConfiguration section */ 440 | 441 | /* Begin XCConfigurationList section */ 442 | CC89B4121D9FE314006BFD6E /* Build configuration list for PBXProject "Cloudy" */ = { 443 | isa = XCConfigurationList; 444 | buildConfigurations = ( 445 | CC89B4271D9FE314006BFD6E /* Debug */, 446 | CC89B4281D9FE314006BFD6E /* Release */, 447 | ); 448 | defaultConfigurationIsVisible = 0; 449 | defaultConfigurationName = Release; 450 | }; 451 | CC89B4291D9FE314006BFD6E /* Build configuration list for PBXNativeTarget "Cloudy" */ = { 452 | isa = XCConfigurationList; 453 | buildConfigurations = ( 454 | CC89B42A1D9FE314006BFD6E /* Debug */, 455 | CC89B42B1D9FE314006BFD6E /* Release */, 456 | ); 457 | defaultConfigurationIsVisible = 0; 458 | defaultConfigurationName = Release; 459 | }; 460 | /* End XCConfigurationList section */ 461 | }; 462 | rootObject = CC89B40F1D9FE314006BFD6E /* Project object */; 463 | } 464 | -------------------------------------------------------------------------------- /Cloudy.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Cloudy/Application Delegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Configure Window 18 | window?.tintColor = UIColor(red:0.99, green:0.47, blue:0.44, alpha:1.0) 19 | 20 | return true 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Cloudy/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Defaults { 12 | 13 | static let Latitude: Double = 51.400592 14 | static let Longitude: Double = 4.760970 15 | 16 | } 17 | 18 | struct API { 19 | 20 | static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 21 | static let BaseURL = URL(string: "https://api.darksky.net/forecast/")! 22 | 23 | static var AuthenticatedBaseURL: URL { 24 | return BaseURL.appendingPathComponent(APIKey) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Cloudy/Extensions/Conversions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversions.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 03/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | 13 | func toCelcius() -> Double { 14 | return ((self - 32.0) / 1.8) 15 | } 16 | 17 | func toKPH() -> Double { 18 | return (self * 1.609344) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Cloudy/Extensions/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 03/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum TimeNotation: Int { 12 | case twelveHour 13 | case twentyFourHour 14 | } 15 | 16 | enum UnitsNotation: Int { 17 | case imperial 18 | case metric 19 | } 20 | 21 | enum TemperatureNotation: Int { 22 | case fahrenheit 23 | case celsius 24 | } 25 | 26 | struct UserDefaultsKeys { 27 | static let timeNotation = "timeNotation" 28 | static let unitsNotation = "unitsNotation" 29 | static let temperatureNotation = "temperatureNotation" 30 | } 31 | 32 | extension UserDefaults { 33 | 34 | // MARK: - Timer Notation 35 | 36 | static func timeNotation() -> TimeNotation { 37 | let storedValue = UserDefaults.standard.integer(forKey: UserDefaultsKeys.timeNotation) 38 | return TimeNotation(rawValue: storedValue) ?? TimeNotation.twelveHour 39 | } 40 | 41 | static func setTimeNotation(timeNotation: TimeNotation) { 42 | UserDefaults.standard.set(timeNotation.rawValue, forKey: UserDefaultsKeys.timeNotation) 43 | } 44 | 45 | // MARK: - Units Notation 46 | 47 | static func unitsNotation() -> UnitsNotation { 48 | let storedValue = UserDefaults.standard.integer(forKey: UserDefaultsKeys.unitsNotation) 49 | return UnitsNotation(rawValue: storedValue) ?? UnitsNotation.imperial 50 | } 51 | 52 | static func setUnitsNotation(unitsNotation: UnitsNotation) { 53 | UserDefaults.standard.set(unitsNotation.rawValue, forKey: UserDefaultsKeys.unitsNotation) 54 | } 55 | 56 | // MARK: - Temperature Notation 57 | 58 | static func temperatureNotation() -> TemperatureNotation { 59 | let storedValue = UserDefaults.standard.integer(forKey: UserDefaultsKeys.temperatureNotation) 60 | return TemperatureNotation(rawValue: storedValue) ?? TemperatureNotation.fahrenheit 61 | } 62 | 63 | static func setTemperatureNotation(temperatureNotation: TemperatureNotation) { 64 | UserDefaults.standard.set(temperatureNotation.rawValue, forKey: UserDefaultsKeys.temperatureNotation) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Cloudy/Managers/DataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum DataManagerError: Error { 12 | 13 | case unknown 14 | case failedRequest 15 | case invalidResponse 16 | 17 | } 18 | 19 | final class DataManager { 20 | 21 | typealias WeatherDataCompletion = (WeatherData?, DataManagerError?) -> () 22 | 23 | // MARK: - Properties 24 | 25 | private let baseURL: URL 26 | 27 | // MARK: - Initialization 28 | 29 | init(baseURL: URL) { 30 | self.baseURL = baseURL 31 | } 32 | 33 | // MARK: - Requesting Data 34 | 35 | func weatherDataForLocation(latitude: Double, longitude: Double, completion: @escaping WeatherDataCompletion) { 36 | // Create URL 37 | let URL = baseURL.appendingPathComponent("\(latitude),\(longitude)") 38 | 39 | // Create Data Task 40 | URLSession.shared.dataTask(with: URL) { (data, response, error) in 41 | DispatchQueue.main.async { 42 | self.didFetchWeatherData(data: data, response: response, error: error, completion: completion) 43 | } 44 | }.resume() 45 | } 46 | 47 | // MARK: - Helper Methods 48 | 49 | private func didFetchWeatherData(data: Data?, response: URLResponse?, error: Error?, completion: WeatherDataCompletion) { 50 | if let _ = error { 51 | completion(nil, .failedRequest) 52 | 53 | } else if let data = data, let response = response as? HTTPURLResponse { 54 | if response.statusCode == 200 { 55 | do { 56 | // Decode JSON 57 | let weatherData: WeatherData = try JSONDecoder.decode(data: data) 58 | 59 | // Invoke Completion Handler 60 | completion(weatherData, nil) 61 | 62 | } catch { 63 | // Invoke Completion Handler 64 | completion(nil, .invalidResponse) 65 | } 66 | 67 | } else { 68 | completion(nil, .failedRequest) 69 | } 70 | 71 | } else { 72 | completion(nil, .unknown) 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Cloudy/Models/WeatherData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherData.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct WeatherData { 12 | 13 | let time: Date 14 | 15 | let lat: Double 16 | let long: Double 17 | let windSpeed: Double 18 | let temperature: Double 19 | 20 | let icon: String 21 | let summary: String 22 | 23 | let dailyData: [WeatherDayData] 24 | 25 | } 26 | 27 | extension WeatherData: JSONDecodable { 28 | 29 | init(decoder: JSONDecoder) throws { 30 | self.lat = try decoder.decode(key: "latitude") 31 | self.long = try decoder.decode(key: "longitude") 32 | self.dailyData = try decoder.decode(key: "daily.data") 33 | 34 | self.icon = try decoder.decode(key: "currently.icon") 35 | self.summary = try decoder.decode(key: "currently.summary") 36 | self.windSpeed = try decoder.decode(key: "currently.windSpeed") 37 | self.temperature = try decoder.decode(key: "currently.temperature") 38 | 39 | let time: Double = try decoder.decode(key: "currently.time") 40 | self.time = Date(timeIntervalSince1970: time) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Cloudy/Models/WeatherDayData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherDayData.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct WeatherDayData { 12 | 13 | let time: Date 14 | let icon: String 15 | let windSpeed: Double 16 | let temperatureMin: Double 17 | let temperatureMax: Double 18 | 19 | } 20 | 21 | extension WeatherDayData: JSONDecodable { 22 | 23 | init(decoder: JSONDecoder) throws { 24 | self.icon = try decoder.decode(key: "icon") 25 | self.windSpeed = try decoder.decode(key: "windSpeed") 26 | self.temperatureMin = try decoder.decode(key: "temperatureMin") 27 | self.temperatureMax = try decoder.decode(key: "temperatureMax") 28 | 29 | let time: Double = try decoder.decode(key: "time") 30 | self.time = Date(timeIntervalSince1970: time) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Cloudy/Protocols/JSONDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDecodable.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol JSONDecodable { 12 | 13 | init(decoder: JSONDecoder) throws 14 | 15 | } 16 | 17 | enum JSONDecoderError: Error { 18 | case invalidData 19 | case keyNotFound(String) 20 | case keyPathNotFound(String) 21 | } 22 | 23 | struct JSONDecoder { 24 | 25 | typealias JSON = [String: AnyObject] 26 | 27 | // MARK: - Properties 28 | 29 | private let JSONData: JSON 30 | 31 | // MARK: - Static Methods 32 | 33 | static func decode(data: Data) throws -> T { 34 | let decoder = try JSONDecoder(data: data) 35 | return try T(decoder: decoder) 36 | } 37 | 38 | // MARK: - Initialization 39 | 40 | init(data: Data) throws { 41 | if let JSONData = try JSONSerialization.jsonObject(with: data, options: []) as? JSON { 42 | self.JSONData = JSONData 43 | } else { 44 | throw JSONDecoderError.invalidData 45 | } 46 | } 47 | 48 | // MARK: - 49 | 50 | private init(JSONData: JSON) { 51 | self.JSONData = JSONData 52 | } 53 | 54 | // MARK: - Public Interface 55 | 56 | func decode(key: String) throws -> T { 57 | if key.contains(".") { 58 | return try value(forKeyPath: key) 59 | } 60 | 61 | guard let value: T = try? value(forKey: key) else { throw JSONDecoderError.keyNotFound(key) } 62 | return value 63 | } 64 | 65 | func decode(key: String) throws -> [T] { 66 | if key.contains(".") { 67 | return try value(forKeyPath: key) 68 | } 69 | 70 | guard let value: [T] = try? value(forKey: key) else { throw JSONDecoderError.keyNotFound(key) } 71 | return value 72 | } 73 | 74 | // MARK: - Private Interface 75 | 76 | private func value(forKey key: String) throws -> T { 77 | guard let value = JSONData[key] as? T else { throw JSONDecoderError.keyNotFound(key) } 78 | return value 79 | } 80 | 81 | private func value(forKey key: String) throws -> [T] { 82 | if let value = JSONData[key] as? [JSON] { 83 | return try value.map({ (partial) -> T in 84 | let decoder = JSONDecoder(JSONData: partial) 85 | return try T(decoder: decoder) 86 | }) 87 | } 88 | 89 | throw JSONDecoderError.keyNotFound(key) 90 | } 91 | 92 | // MARK: - 93 | 94 | private func value(forKeyPath keyPath: String) throws -> T { 95 | var partial = JSONData 96 | let keys = keyPath.components(separatedBy: ".") 97 | 98 | for i in 0..(forKeyPath keyPath: String) throws -> [T] { 115 | var partial = JSONData 116 | let keys = keyPath.components(separatedBy: ".") 117 | 118 | for i in 0.. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Cloudy/Storyboards/Main.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 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 141 | 142 | 143 | 148 | 153 | 158 | 163 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 283 | 288 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | -------------------------------------------------------------------------------- /Cloudy/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | NSLocationWhenInUseUsageDescription 32 | Cloudy needs your location to fetch weather data for your location. 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/Root View Controller/RootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreLocation 11 | 12 | class RootViewController: UIViewController { 13 | 14 | // MARK: - Constants 15 | 16 | private let segueDayView = "SegueDayView" 17 | private let segueWeekView = "SegueWeekView" 18 | fileprivate let SegueSettingsView = "SegueSettingsView" 19 | 20 | // MARK: - Properties 21 | 22 | @IBOutlet fileprivate var dayViewController: DayViewController! 23 | @IBOutlet fileprivate var weekViewController: WeekViewController! 24 | 25 | // MARK: - 26 | 27 | fileprivate var currentLocation: CLLocation? { 28 | didSet { 29 | fetchWeatherData() 30 | } 31 | } 32 | 33 | fileprivate lazy var dataManager = { 34 | return DataManager(baseURL: API.AuthenticatedBaseURL) 35 | }() 36 | 37 | private lazy var locationManager: CLLocationManager = { 38 | // Initialize Location Manager 39 | let locationManager = CLLocationManager() 40 | 41 | // Configure Location Manager 42 | locationManager.distanceFilter = 1000.0 43 | locationManager.desiredAccuracy = 1000.0 44 | 45 | return locationManager 46 | }() 47 | 48 | // MARK: - View Life Cycle 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | setupView() 54 | setupNotificationHandling() 55 | } 56 | 57 | // MARK: - Navigation 58 | 59 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 60 | guard let identifier = segue.identifier else { return } 61 | 62 | switch identifier { 63 | case segueDayView: 64 | if let dayViewController = segue.destination as? DayViewController { 65 | self.dayViewController = dayViewController 66 | 67 | // Configure Day View Controller 68 | self.dayViewController.delegate = self 69 | 70 | } else { 71 | fatalError("Unexpected Destination View Controller") 72 | } 73 | case segueWeekView: 74 | if let weekViewController = segue.destination as? WeekViewController { 75 | self.weekViewController = weekViewController 76 | 77 | // Configure Day View Controller 78 | self.weekViewController.delegate = self 79 | 80 | } else { 81 | fatalError("Unexpected Destination View Controller") 82 | } 83 | case SegueSettingsView: 84 | if let navigationController = segue.destination as? UINavigationController, 85 | let settingsViewController = navigationController.topViewController as? SettingsViewController { 86 | settingsViewController.delegate = self 87 | } else { 88 | fatalError("Unexpected Destination View Controller") 89 | } 90 | default: break 91 | } 92 | } 93 | 94 | // MARK: - View Methods 95 | 96 | private func setupView() { 97 | 98 | } 99 | 100 | private func updateView() { 101 | 102 | } 103 | 104 | // MARK: - Actions 105 | 106 | @IBAction func unwindToRootViewController(segue: UIStoryboardSegue) { 107 | 108 | } 109 | 110 | // MARK: - Notification Handling 111 | 112 | func applicationDidBecomeActive(notification: Notification) { 113 | requestLocation() 114 | } 115 | 116 | // MARK: - Helper Methods 117 | 118 | private func setupNotificationHandling() { 119 | let notificationCenter = NotificationCenter.default 120 | notificationCenter.addObserver(self, selector: #selector(RootViewController.applicationDidBecomeActive(notification:)), name: Notification.Name.UIApplicationDidBecomeActive, object: nil) 121 | } 122 | 123 | private func requestLocation() { 124 | // Configure Location Manager 125 | locationManager.delegate = self 126 | 127 | if CLLocationManager.authorizationStatus() == .authorizedWhenInUse { 128 | // Request Current Location 129 | locationManager.requestLocation() 130 | 131 | } else { 132 | // Request Authorization 133 | locationManager.requestWhenInUseAuthorization() 134 | } 135 | } 136 | 137 | fileprivate func fetchWeatherData() { 138 | guard let location = currentLocation else { return } 139 | 140 | let latitude = location.coordinate.latitude 141 | let longitude = location.coordinate.longitude 142 | 143 | print("\(latitude), \(longitude)") 144 | 145 | dataManager.weatherDataForLocation(latitude: latitude, longitude: longitude) { (response, error) in 146 | if let error = error { 147 | print(error) 148 | } else if let response = response { 149 | // Configure Day View Controller 150 | self.dayViewController.now = response 151 | 152 | // Configure Week View Controller 153 | self.weekViewController.week = response.dailyData 154 | } 155 | } 156 | } 157 | 158 | } 159 | 160 | extension RootViewController: CLLocationManagerDelegate { 161 | 162 | // MARK: - Authorization 163 | 164 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 165 | if status == .authorizedWhenInUse { 166 | // Request Location 167 | manager.requestLocation() 168 | 169 | } else { 170 | // Fall Back to Default Location 171 | currentLocation = CLLocation(latitude: Defaults.Latitude, longitude: Defaults.Longitude) 172 | } 173 | } 174 | 175 | // MARK: - Location Updates 176 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 177 | if let location = locations.first { 178 | // Update Current Location 179 | currentLocation = location 180 | 181 | // Reset Delegate 182 | manager.delegate = nil 183 | 184 | // Stop Location Manager 185 | manager.stopUpdatingLocation() 186 | 187 | } else { 188 | // Fall Back to Default Location 189 | currentLocation = CLLocation(latitude: Defaults.Latitude, longitude: Defaults.Longitude) 190 | } 191 | } 192 | 193 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 194 | print(error) 195 | 196 | if currentLocation == nil { 197 | // Fall Back to Default Location 198 | currentLocation = CLLocation(latitude: Defaults.Latitude, longitude: Defaults.Longitude) 199 | } 200 | } 201 | 202 | } 203 | 204 | extension RootViewController: DayViewControllerDelegate { 205 | 206 | func controllerDidTapSettingsButton(controller: DayViewController) { 207 | performSegue(withIdentifier: SegueSettingsView, sender: self) 208 | } 209 | 210 | } 211 | 212 | extension RootViewController: WeekViewControllerDelegate { 213 | 214 | func controllerDidRefresh(controller: WeekViewController) { 215 | fetchWeatherData() 216 | } 217 | 218 | } 219 | 220 | extension RootViewController: SettingsViewControllerDelegate { 221 | 222 | func controllerDidChangeTimeNotation(controller: SettingsViewController) { 223 | dayViewController.reloadData() 224 | weekViewController.reloadData() 225 | } 226 | 227 | func controllerDidChangeUnitsNotation(controller: SettingsViewController) { 228 | dayViewController.reloadData() 229 | weekViewController.reloadData() 230 | } 231 | 232 | func controllerDidChangeTemperatureNotation(controller: SettingsViewController) { 233 | dayViewController.reloadData() 234 | weekViewController.reloadData() 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/SettingsViewController/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 03/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol SettingsViewControllerDelegate { 12 | func controllerDidChangeTimeNotation(controller: SettingsViewController) 13 | func controllerDidChangeUnitsNotation(controller: SettingsViewController) 14 | func controllerDidChangeTemperatureNotation(controller: SettingsViewController) 15 | } 16 | 17 | class SettingsViewController: UIViewController { 18 | 19 | // MARK: - Properties 20 | 21 | @IBOutlet var tableView: UITableView! 22 | 23 | // MARK: - 24 | 25 | var delegate: SettingsViewControllerDelegate? 26 | 27 | // MARK: - View Life Cycle 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | title = "Settings" 33 | 34 | setupView() 35 | } 36 | 37 | // MARK: - View Methods 38 | 39 | private func setupView() { 40 | setupTableView() 41 | } 42 | 43 | private func updateView() { 44 | 45 | } 46 | 47 | // MARK: - 48 | 49 | private func setupTableView() { 50 | tableView.separatorInset = UIEdgeInsets.zero 51 | } 52 | 53 | } 54 | 55 | extension SettingsViewController: UITableViewDataSource, UITableViewDelegate { 56 | 57 | private enum Section: Int { 58 | case time 59 | case units 60 | case temperature 61 | 62 | var numberOfRows: Int { 63 | return 2 64 | } 65 | 66 | static var count: Int { 67 | return (Section.temperature.rawValue + 1) 68 | } 69 | 70 | } 71 | 72 | // MARK: - Table View Data Source Methods 73 | 74 | func numberOfSections(in tableView: UITableView) -> Int { 75 | return Section.count 76 | } 77 | 78 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 79 | guard let section = Section(rawValue: section) else { fatalError("Unexpected Section") } 80 | return section.numberOfRows 81 | } 82 | 83 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 84 | guard let section = Section(rawValue: indexPath.section) else { fatalError("Unexpected Section") } 85 | guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell else { fatalError("Unexpected Table View Cell") } 86 | 87 | switch section { 88 | case .time: 89 | cell.mainLabel.text = (indexPath.row == 0) ? "12 Hour" : "24 Hour" 90 | 91 | let timeNotation = UserDefaults.timeNotation() 92 | if indexPath.row == timeNotation.rawValue { 93 | cell.accessoryType = .checkmark 94 | } else { 95 | cell.accessoryType = .none 96 | } 97 | case .units: 98 | cell.mainLabel.text = (indexPath.row == 0) ? "Imperial" : "Metric" 99 | 100 | let unitsNotation = UserDefaults.unitsNotation() 101 | if indexPath.row == unitsNotation.rawValue { 102 | cell.accessoryType = .checkmark 103 | } else { 104 | cell.accessoryType = .none 105 | } 106 | case .temperature: 107 | cell.mainLabel.text = (indexPath.row == 0) ? "Fahrenheit" : "Celcius" 108 | 109 | let temperatureNotation = UserDefaults.temperatureNotation() 110 | if indexPath.row == temperatureNotation.rawValue { 111 | cell.accessoryType = .checkmark 112 | } else { 113 | cell.accessoryType = .none 114 | } 115 | } 116 | 117 | return cell 118 | } 119 | 120 | // MARK: - Table View Delegate Methods 121 | 122 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 123 | tableView.deselectRow(at: indexPath, animated: true) 124 | 125 | guard let section = Section(rawValue: indexPath.section) else { fatalError("Unexpected Section") } 126 | 127 | switch section { 128 | case .time: 129 | let timeNotation = UserDefaults.timeNotation() 130 | guard indexPath.row != timeNotation.rawValue else { return } 131 | 132 | if let newTimeNotation = TimeNotation(rawValue: indexPath.row) { 133 | // Update User Defaults 134 | UserDefaults.setTimeNotation(timeNotation: newTimeNotation) 135 | 136 | // Notify Delegate 137 | delegate?.controllerDidChangeTimeNotation(controller: self) 138 | } 139 | case .units: 140 | let unitsNotation = UserDefaults.unitsNotation() 141 | guard indexPath.row != unitsNotation.rawValue else { return } 142 | 143 | if let newUnitsNotation = UnitsNotation(rawValue: indexPath.row) { 144 | // Update User Defaults 145 | UserDefaults.setUnitsNotation(unitsNotation: newUnitsNotation) 146 | 147 | // Notify Delegate 148 | delegate?.controllerDidChangeUnitsNotation(controller: self) 149 | } 150 | case .temperature: 151 | let temperatureNotation = UserDefaults.temperatureNotation() 152 | guard indexPath.row != temperatureNotation.rawValue else { return } 153 | 154 | if let newTemperatureNotation = TemperatureNotation(rawValue: indexPath.row) { 155 | // Update User Defaults 156 | UserDefaults.setTemperatureNotation(temperatureNotation: newTemperatureNotation) 157 | 158 | // Notify Delegate 159 | delegate?.controllerDidChangeTemperatureNotation(controller: self) 160 | } 161 | } 162 | 163 | tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none) 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/SettingsViewController/Table View Cells/SettingsTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsTableViewCell.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 03/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SettingsTableViewCell: UITableViewCell { 12 | 13 | // MARK: - Type Properties 14 | 15 | static let reuseIdentifier = "SettingsCell" 16 | 17 | // MARK: - Properties 18 | 19 | @IBOutlet var mainLabel: UILabel! 20 | 21 | // MARK: - Initialization 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | 26 | // Configure Cell 27 | selectionStyle = .none 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/Weather View Controllers/DayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DayViewController.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol DayViewControllerDelegate { 12 | func controllerDidTapSettingsButton(controller: DayViewController) 13 | } 14 | 15 | class DayViewController: WeatherViewController { 16 | 17 | // MARK: - Properties 18 | 19 | @IBOutlet var dateLabel: UILabel! 20 | @IBOutlet var timeLabel: UILabel! 21 | @IBOutlet var windSpeedLabel: UILabel! 22 | @IBOutlet var temperatureLabel: UILabel! 23 | @IBOutlet var descriptionLabel: UILabel! 24 | @IBOutlet var iconImageView: UIImageView! 25 | 26 | // MARK: - 27 | 28 | var delegate: DayViewControllerDelegate? 29 | 30 | // MARK: - 31 | 32 | var now: WeatherData? { 33 | didSet { 34 | updateView() 35 | } 36 | } 37 | 38 | // MARK: - View Life Cycle 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | setupView() 44 | } 45 | 46 | // MARK: - Public Interface 47 | 48 | override func reloadData() { 49 | updateView() 50 | } 51 | 52 | // MARK: - View Methods 53 | 54 | private func setupView() { 55 | 56 | } 57 | 58 | private func updateView() { 59 | activityIndicatorView.stopAnimating() 60 | 61 | if let now = now { 62 | updateWeatherDataContainer(withWeatherData: now) 63 | 64 | } else { 65 | messageLabel.isHidden = false 66 | messageLabel.text = "Cloudy was unable to fetch weather data." 67 | 68 | } 69 | } 70 | 71 | // MARK: - 72 | 73 | private func updateWeatherDataContainer(withWeatherData weatherData: WeatherData) { 74 | weatherDataContainer.isHidden = false 75 | 76 | var windSpeed = weatherData.windSpeed 77 | var temperature = weatherData.temperature 78 | 79 | let dateFormatter = DateFormatter() 80 | dateFormatter.dateFormat = "EEE, MMMM d" 81 | dateLabel.text = dateFormatter.string(from: weatherData.time) 82 | 83 | let timeFormatter = DateFormatter() 84 | 85 | if UserDefaults.timeNotation() == .twelveHour { 86 | timeFormatter.dateFormat = "hh:mm a" 87 | } else { 88 | timeFormatter.dateFormat = "HH:mm" 89 | } 90 | 91 | timeLabel.text = timeFormatter.string(from: weatherData.time) 92 | 93 | descriptionLabel.text = weatherData.summary 94 | 95 | if UserDefaults.temperatureNotation() != .fahrenheit { 96 | temperature = temperature.toCelcius() 97 | temperatureLabel.text = String(format: "%.1f °C", temperature) 98 | } else { 99 | temperatureLabel.text = String(format: "%.1f °F", temperature) 100 | } 101 | 102 | if UserDefaults.unitsNotation() != .imperial { 103 | windSpeed = windSpeed.toKPH() 104 | windSpeedLabel.text = String(format: "%.f KPH", windSpeed) 105 | } else { 106 | windSpeedLabel.text = String(format: "%.f MPH", windSpeed) 107 | } 108 | 109 | iconImageView.image = imageForIcon(withName: weatherData.icon) 110 | } 111 | 112 | // MARK: - Actions 113 | 114 | @IBAction func didTapSettingsButton(sender: UIButton) { 115 | delegate?.controllerDidTapSettingsButton(controller: self) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/Weather View Controllers/Table View Cells/WeatherDayTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherDayTableViewCell.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 02/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WeatherDayTableViewCell: UITableViewCell { 12 | 13 | // MARK: - Type Properties 14 | 15 | static let reuseIdentifier = "WeatherDayCell" 16 | 17 | // MARK: - Properties 18 | 19 | @IBOutlet var dayLabel: UILabel! 20 | @IBOutlet var dateLabel: UILabel! 21 | @IBOutlet var windSpeedLabel: UILabel! 22 | @IBOutlet var temperatureLabel: UILabel! 23 | @IBOutlet var iconImageView: UIImageView! 24 | 25 | // MARK: - Initialization 26 | 27 | override func awakeFromNib() { 28 | super.awakeFromNib() 29 | 30 | // Configure Cell 31 | selectionStyle = .none 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/Weather View Controllers/WeatherViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherViewController.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WeatherViewController: UIViewController { 12 | 13 | // MARK: - Properties 14 | 15 | @IBOutlet var messageLabel: UILabel! 16 | @IBOutlet var weatherDataContainer: UIView! 17 | @IBOutlet var activityIndicatorView: UIActivityIndicatorView! 18 | 19 | // MARK: - View Life Cycle 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | setupView() 25 | } 26 | 27 | // MARK: - Public Interface 28 | 29 | func reloadData() { 30 | 31 | } 32 | 33 | // MARK: - View Methods 34 | 35 | private func setupView() { 36 | // Configure Message Label 37 | messageLabel.isHidden = true 38 | 39 | // Configure Weather Data Container 40 | weatherDataContainer.isHidden = true 41 | 42 | // Configure Activity Indicator View 43 | activityIndicatorView.startAnimating() 44 | activityIndicatorView.hidesWhenStopped = true 45 | } 46 | 47 | private func updateView() { 48 | 49 | } 50 | 51 | // MARK: - Helper Methods 52 | 53 | func imageForIcon(withName name: String) -> UIImage? { 54 | switch name { 55 | case "clear-day": 56 | return UIImage(named: "clear-day") 57 | case "clear-night": 58 | return UIImage(named: "clear-night") 59 | case "rain": 60 | return UIImage(named: "rain") 61 | case "snow": 62 | return UIImage(named: "snow") 63 | case "sleet": 64 | return UIImage(named: "sleet") 65 | case "wind", "cloudy", "partly-cloudy-day", "partly-cloudy-night": 66 | return UIImage(named: "cloudy") 67 | default: 68 | return UIImage(named: "clear-day") 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Cloudy/View Controllers/Weather View Controllers/WeekViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeekViewController.swift 3 | // Cloudy 4 | // 5 | // Created by Bart Jacobs on 01/10/16. 6 | // Copyright © 2016 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol WeekViewControllerDelegate { 12 | func controllerDidRefresh(controller: WeekViewController) 13 | } 14 | 15 | class WeekViewController: WeatherViewController { 16 | 17 | // MARK: - Properties 18 | 19 | @IBOutlet var tableView: UITableView! 20 | 21 | // MARK: - 22 | 23 | var delegate: WeekViewControllerDelegate? 24 | 25 | // MARK: - 26 | 27 | var week: [WeatherDayData]? { 28 | didSet { 29 | updateView() 30 | } 31 | } 32 | 33 | // MARK: - 34 | 35 | fileprivate lazy var dayFormatter: DateFormatter = { 36 | let dateFormatter = DateFormatter() 37 | dateFormatter.dateFormat = "EEEE" 38 | return dateFormatter 39 | }() 40 | 41 | fileprivate lazy var dateFormatter: DateFormatter = { 42 | let dateFormatter = DateFormatter() 43 | dateFormatter.dateFormat = "MMMM d" 44 | return dateFormatter 45 | }() 46 | 47 | // MARK: - View Life Cycle 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | 52 | setupView() 53 | } 54 | 55 | // MARK: - Public Interface 56 | 57 | override func reloadData() { 58 | updateView() 59 | } 60 | 61 | // MARK: - View Methods 62 | 63 | private func setupView() { 64 | setupTableView() 65 | setupRefreshControl() 66 | } 67 | 68 | private func updateView() { 69 | activityIndicatorView.stopAnimating() 70 | tableView.refreshControl?.endRefreshing() 71 | 72 | if let week = week { 73 | updateWeatherDataContainer(withWeatherData: week) 74 | 75 | } else { 76 | messageLabel.isHidden = false 77 | messageLabel.text = "Cloudy was unable to fetch weather data." 78 | 79 | } 80 | } 81 | 82 | // MARK: - 83 | 84 | private func setupTableView() { 85 | tableView.separatorInset = UIEdgeInsets.zero 86 | } 87 | 88 | private func setupRefreshControl() { 89 | // Initialize Refresh Control 90 | let refreshControl = UIRefreshControl() 91 | 92 | // Configure Refresh Control 93 | refreshControl.addTarget(self, action: #selector(WeekViewController.didRefresh(sender:)), for: .valueChanged) 94 | 95 | // Update Table View) 96 | tableView.refreshControl = refreshControl 97 | } 98 | 99 | // MARK: - 100 | 101 | private func updateWeatherDataContainer(withWeatherData weatherData: [WeatherDayData]) { 102 | weatherDataContainer.isHidden = false 103 | 104 | tableView.reloadData() 105 | } 106 | 107 | // MARK: - Actions 108 | 109 | func didRefresh(sender: UIRefreshControl) { 110 | delegate?.controllerDidRefresh(controller: self) 111 | } 112 | 113 | } 114 | 115 | extension WeekViewController: UITableViewDataSource { 116 | 117 | func numberOfSections(in tableView: UITableView) -> Int { 118 | return week == nil ? 0 : 1 119 | } 120 | 121 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 122 | guard let week = week else { return 0 } 123 | return week.count 124 | } 125 | 126 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 127 | guard let cell = tableView.dequeueReusableCell(withIdentifier: WeatherDayTableViewCell.reuseIdentifier, for: indexPath) as? WeatherDayTableViewCell else { fatalError("Unexpected Table View Cell") } 128 | 129 | if let week = week { 130 | // Fetch Weather Data 131 | let weatherData = week[indexPath.row] 132 | 133 | var windSpeed = weatherData.windSpeed 134 | var temperatureMin = weatherData.temperatureMin 135 | var temperatureMax = weatherData.temperatureMax 136 | 137 | if UserDefaults.temperatureNotation() != .fahrenheit { 138 | temperatureMin = temperatureMin.toCelcius() 139 | temperatureMax = temperatureMax.toCelcius() 140 | } 141 | 142 | // Configure Cell 143 | cell.dayLabel.text = dayFormatter.string(from: weatherData.time) 144 | cell.dateLabel.text = dateFormatter.string(from: weatherData.time) 145 | 146 | let min = String(format: "%.0f°", temperatureMin) 147 | let max = String(format: "%.0f°", temperatureMax) 148 | 149 | cell.temperatureLabel.text = "\(min) - \(max)" 150 | 151 | if UserDefaults.unitsNotation() != .imperial { 152 | windSpeed = windSpeed.toKPH() 153 | cell.windSpeedLabel.text = String(format: "%.f KPH", windSpeed) 154 | } else { 155 | cell.windSpeedLabel.text = String(format: "%.f MPH", windSpeed) 156 | } 157 | 158 | cell.iconImageView.image = imageForIcon(withName: weatherData.icon) 159 | } 160 | 161 | return cell 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Mastering Model-View-Viewmodel With Swift 2 | 3 | #### Author: Bart Jacobs 4 | 5 | In [Mastering Model-View-Viewmodel With Swift](https://cocoacasts.com/series/mastering-mvvm-with-swift), you learn the differences between Model-View-Controller and Model-View-ViewModel, highlighting the benefits Model-View-ViewModel has over Model-View-Controller. 6 | 7 | After a short introduction, we take an application built with Model-View-Controller and refactor it to use Model-View-ViewModel. Along the way, you learn about the anatomy of view models, how to create them, and how to test them. We also add protocols to the mix to further simplify the view controllers in the project. 8 | 9 | At the end of this series, you have the knowledge and hands-on experience to apply Model-View-ViewModel in your own projects. 10 | 11 | Take me to the **[Mastering Model-View-Viewmodel With Swift.](https://cocoacasts.com/series/mastering-mvvm-with-swift)** 12 | --------------------------------------------------------------------------------