├── sunshine
├── Assets.xcassets
│ ├── Contents.json
│ ├── Colors
│ │ ├── Contents.json
│ │ ├── Pink Dawn.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Dusk.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Mid Day.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Night.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Dawn End Gradient.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Mid Day End Gradient.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Night End Gradient.colorset
│ │ │ └── Contents.json
│ │ ├── Peach Dusk End Gradient.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Dawn Shadow Card.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Mid Day Shadow Card.colorset
│ │ │ └── Contents.json
│ │ ├── Blue Night Shadow Card.colorset
│ │ │ └── Contents.json
│ │ └── Peach Dusk Shadow Card.colorset
│ │ │ └── Contents.json
│ ├── Moon.imageset
│ │ ├── Moon@1x.png
│ │ ├── Moon@2x.png
│ │ ├── Moon@3x.png
│ │ └── Contents.json
│ ├── cloud.imageset
│ │ ├── cloud@3x.png
│ │ └── Contents.json
│ ├── drop.imageset
│ │ ├── drop@3x.png
│ │ └── Contents.json
│ ├── clouds.imageset
│ │ ├── clouds@3x.png
│ │ └── Contents.json
│ ├── sun-mid.imageset
│ │ ├── sun-mid@1x.png
│ │ ├── sun-mid@2x.png
│ │ ├── sun-mid@3x.png
│ │ └── Contents.json
│ ├── sun-dawn.imageset
│ │ ├── sun-dawn@1x.png
│ │ ├── sun-dawn@2x.png
│ │ ├── sun-dawn@3x.png
│ │ └── Contents.json
│ ├── big-cloud.imageset
│ │ ├── Big Cloud@3x.png
│ │ └── Contents.json
│ ├── heavy-rain.imageset
│ │ ├── Heavy Rain@3x.png
│ │ └── Contents.json
│ ├── light-drop.imageset
│ │ ├── light drop@3x.png
│ │ └── Contents.json
│ ├── light-rain.imageset
│ │ ├── Light Rain@3x.png
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── iPhone_App_60_2x.png
│ │ ├── iPhone_App_60_3x.png
│ │ ├── App_store_1024_1x.png
│ │ ├── iPhone_Settings_29_2x.png
│ │ ├── iPhone_Settings_29_3x.png
│ │ ├── iPhone_Spotlight_40_2x.png
│ │ ├── iPhone_Spotlight_40_3x.png
│ │ ├── iPhone_Notifications_20_2x.png
│ │ ├── iPhone_Notifications_20_3x.png
│ │ └── Contents.json
│ ├── heavy-rain-drops.imageset
│ │ ├── heavy rain drops@3x.png
│ │ └── Contents.json
│ ├── light-rain-drops.imageset
│ │ ├── Light rain drops@3x.png
│ │ └── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── humidity.imageset
│ │ ├── Contents.json
│ │ └── humidity.svg
│ ├── menu-dark.imageset
│ │ ├── Contents.json
│ │ └── menu.svg
│ └── menu-light.imageset
│ │ ├── Contents.json
│ │ └── menu-light.svg
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── sunshine.xcdatamodeld
│ ├── .xccurrentversion
│ └── sunshine.xcdatamodel
│ │ └── contents
├── sunshineApp.swift
├── Info.plist
├── LocationService.swift
├── SearchBar.swift
├── Persistence.swift
├── WeatherAPIClient.swift
├── SunDial.swift
├── Settings.swift
├── Forecast.swift
└── ContentView.swift
├── sunshine.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── maxime.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── sunshineTests
├── Info.plist
└── sunshineTests.swift
├── sunshineUITests
├── Info.plist
└── sunshineUITests.swift
└── README.md
/sunshine/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sunshine/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Moon.imageset/Moon@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/Moon.imageset/Moon@1x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Moon.imageset/Moon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/Moon.imageset/Moon@2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Moon.imageset/Moon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/Moon.imageset/Moon@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/cloud.imageset/cloud@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/cloud.imageset/cloud@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/drop.imageset/drop@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/drop.imageset/drop@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/clouds.imageset/clouds@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/clouds.imageset/clouds@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-mid.imageset/sun-mid@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/sun-mid.imageset/sun-mid@1x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-mid.imageset/sun-mid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/sun-mid.imageset/sun-mid@2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-mid.imageset/sun-mid@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/sun-mid.imageset/sun-mid@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-dawn.imageset/sun-dawn@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/sun-dawn.imageset/sun-dawn@1x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-dawn.imageset/sun-dawn@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/sun-dawn.imageset/sun-dawn@2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-dawn.imageset/sun-dawn@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/sun-dawn.imageset/sun-dawn@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/big-cloud.imageset/Big Cloud@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/big-cloud.imageset/Big Cloud@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/heavy-rain.imageset/Heavy Rain@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/heavy-rain.imageset/Heavy Rain@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/light-drop.imageset/light drop@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/light-drop.imageset/light drop@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/light-rain.imageset/Light Rain@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/light-rain.imageset/Light Rain@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/App_store_1024_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/App_store_1024_1x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/heavy-rain-drops.imageset/heavy rain drops@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/heavy-rain-drops.imageset/heavy rain drops@3x.png
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/light-rain-drops.imageset/Light rain drops@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximeHeckel/sunshine-weather-app/HEAD/sunshine/Assets.xcassets/light-rain-drops.imageset/Light rain drops@3x.png
--------------------------------------------------------------------------------
/sunshine.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sunshine/sunshine.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | sunshine.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sunshine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/drop.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "drop@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/cloud.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "cloud@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/clouds.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "clouds@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/humidity.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "humidity.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/menu-dark.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "menu.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/big-cloud.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "Big Cloud@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/menu-light.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "menu-light.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/heavy-rain.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "Heavy Rain@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/light-drop.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "light drop@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/light-rain.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "Light Rain@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/heavy-rain-drops.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "heavy rain drops@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/light-rain-drops.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "Light rain drops@3x.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sunshine/sunshineApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // sunshineApp.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 11/26/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct sunshineApp: App {
12 | let persistenceController = PersistenceController.shared
13 |
14 | var body: some Scene {
15 | WindowGroup {
16 | ContentView()
17 | .environment(\.managedObjectContext, persistenceController.container.viewContext)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/menu-dark.imageset/menu.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/menu-light.imageset/menu-light.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Moon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Moon@1x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "Moon@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "Moon@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-mid.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "sun-mid@1x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "sun-mid@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "sun-mid@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/sun-dawn.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "sun-dawn@1x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "sun-dawn@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "sun-dawn@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/humidity.imageset/humidity.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/sunshineTests/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 |
--------------------------------------------------------------------------------
/sunshineUITests/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 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Pink Dawn.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xC0",
9 | "green" : "0xBF",
10 | "red" : "0xFF"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Dusk.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.831",
10 | "red" : "0.839"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Mid Day.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.820",
10 | "red" : "0.667"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Night.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.306",
9 | "green" : "0.137",
10 | "red" : "0.027"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine.xcodeproj/xcuserdata/maxime.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | sunshine.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 9C79929F25701B180072BEE2
16 |
17 | primary
18 |
19 |
20 | 9C7992B525701B1D0072BEE2
21 |
22 | primary
23 |
24 |
25 | 9C7992C025701B1D0072BEE2
26 |
27 | primary
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Dawn End Gradient.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFF",
9 | "green" : "0xE2",
10 | "red" : "0xCE"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Mid Day End Gradient.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.914",
10 | "red" : "0.855"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Night End Gradient.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.651",
9 | "green" : "0.290",
10 | "red" : "0.106"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Peach Dusk End Gradient.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.812",
9 | "green" : "0.875",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sunshine
2 |
3 | This is my first SwiftUI project, as I decided not to release it to the App Store. I made the codebase available here for anyone to take a look at.
4 | It can serve as a good example or template to anyone looking to build a weather app.
5 |
6 | ## Instructions
7 |
8 | You can try out the app on your own device. For that you will need to:
9 |
10 | 1. Register to [OpenWeatherMap](https://openweathermap.org) and get a token for the **One Call API** (very important as the app is based on the spec of this API)
11 | 2. Clone this repository
12 | 3. Add the OpenWeatherMap token to `WeatherAPIClient.swift` line 145
13 | 4. Compile and run the app on a device or the simulator
14 |
15 | ## Notes
16 |
17 | - Sunshine works almost flawlessly from iOS 14 to iOS 14.3.
18 | - This project is stopped, so I won't do any maintenance. I won't reply to most issues if any at all.
19 | - Some weather assets are missing, some others are not animated, this is because I stopped working on this project before being able to tackle those.
20 |
21 |
--------------------------------------------------------------------------------
/sunshineTests/sunshineTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // sunshineTests.swift
3 | // sunshineTests
4 | //
5 | // Created by Maxime on 11/26/20.
6 | //
7 |
8 | import XCTest
9 | @testable import sunshine
10 |
11 | class sunshineTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | self.measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Dawn Shadow Card.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | },
6 | {
7 | "appearances" : [
8 | {
9 | "appearance" : "luminosity",
10 | "value" : "light"
11 | }
12 | ],
13 | "color" : {
14 | "color-space" : "srgb",
15 | "components" : {
16 | "alpha" : "1.000",
17 | "blue" : "1.000",
18 | "green" : "0.961",
19 | "red" : "0.937"
20 | }
21 | },
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "color" : {
32 | "color-space" : "srgb",
33 | "components" : {
34 | "alpha" : "0.650",
35 | "blue" : "1.000",
36 | "green" : "0.961",
37 | "red" : "0.937"
38 | }
39 | },
40 | "idiom" : "universal"
41 | }
42 | ],
43 | "info" : {
44 | "author" : "xcode",
45 | "version" : 1
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Mid Day Shadow Card.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | },
6 | {
7 | "appearances" : [
8 | {
9 | "appearance" : "luminosity",
10 | "value" : "light"
11 | }
12 | ],
13 | "color" : {
14 | "color-space" : "srgb",
15 | "components" : {
16 | "alpha" : "1.000",
17 | "blue" : "1.000",
18 | "green" : "0.965",
19 | "red" : "0.937"
20 | }
21 | },
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "color" : {
32 | "color-space" : "srgb",
33 | "components" : {
34 | "alpha" : "0.650",
35 | "blue" : "1.000",
36 | "green" : "0.965",
37 | "red" : "0.937"
38 | }
39 | },
40 | "idiom" : "universal"
41 | }
42 | ],
43 | "info" : {
44 | "author" : "xcode",
45 | "version" : 1
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Blue Night Shadow Card.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | },
6 | {
7 | "appearances" : [
8 | {
9 | "appearance" : "luminosity",
10 | "value" : "light"
11 | }
12 | ],
13 | "color" : {
14 | "color-space" : "srgb",
15 | "components" : {
16 | "alpha" : "0.500",
17 | "blue" : "0.780",
18 | "green" : "0.659",
19 | "red" : "0.592"
20 | }
21 | },
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "color" : {
32 | "color-space" : "srgb",
33 | "components" : {
34 | "alpha" : "0.500",
35 | "blue" : "0.780",
36 | "green" : "0.659",
37 | "red" : "0.592"
38 | }
39 | },
40 | "idiom" : "universal"
41 | }
42 | ],
43 | "info" : {
44 | "author" : "xcode",
45 | "version" : 1
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/Colors/Peach Dusk Shadow Card.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | },
6 | {
7 | "appearances" : [
8 | {
9 | "appearance" : "luminosity",
10 | "value" : "light"
11 | }
12 | ],
13 | "color" : {
14 | "color-space" : "srgb",
15 | "components" : {
16 | "alpha" : "1.000",
17 | "blue" : "0.933",
18 | "green" : "0.949",
19 | "red" : "1.000"
20 | }
21 | },
22 | "idiom" : "universal"
23 | },
24 | {
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "color" : {
32 | "color-space" : "srgb",
33 | "components" : {
34 | "alpha" : "0.650",
35 | "blue" : "0.933",
36 | "green" : "0.949",
37 | "red" : "1.000"
38 | }
39 | },
40 | "idiom" : "universal"
41 | }
42 | ],
43 | "info" : {
44 | "author" : "xcode",
45 | "version" : 1
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sunshine/sunshine.xcdatamodeld/sunshine.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sunshine/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iPhone_Notifications_20_2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "iPhone_Notifications_20_3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "iPhone_Settings_29_2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "iPhone_Settings_29_3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "iPhone_Spotlight_40_2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "iPhone_Spotlight_40_3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "iPhone_App_60_2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "iPhone_App_60_3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "App_store_1024_1x.png",
53 | "idiom" : "ios-marketing",
54 | "scale" : "1x",
55 | "size" : "1024x1024"
56 | }
57 | ],
58 | "info" : {
59 | "author" : "xcode",
60 | "version" : 1
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/sunshineUITests/sunshineUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // sunshineUITests.swift
3 | // sunshineUITests
4 | //
5 | // Created by Maxime on 11/26/20.
6 | //
7 |
8 | import XCTest
9 |
10 | class sunshineUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/sunshine/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Sunshine
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UIApplicationSupportsIndirectInputEvents
31 |
32 | UILaunchScreen
33 |
34 | UIRequiredDeviceCapabilities
35 |
36 | armv7
37 |
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/sunshine/LocationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationSearchService.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 9/24/20.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import MapKit
11 | import Combine
12 |
13 | /**
14 | This location search service is based on https://www.mozzafiller.com/posts/mklocalsearchcompleter-swiftui-combine
15 | */
16 |
17 | class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
18 | @Published var searchQuery = ""
19 | var completer: MKLocalSearchCompleter
20 | @Published var completions: [MKLocalSearchCompletion] = []
21 | var cancellable: AnyCancellable?
22 |
23 | override init() {
24 | completer = MKLocalSearchCompleter()
25 | super.init()
26 | cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer)
27 | completer.delegate = self
28 | completer.resultTypes = .address
29 | }
30 |
31 | func completer(_ completer: MKLocalSearchCompleter, didFailWithError: Error) {
32 | // Set the results to empty in case the search query is empty or in case there's an uknown error
33 | self.completions = []
34 | }
35 |
36 | func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
37 | // Filter the results
38 | self.completions = completer.results.filter { result in
39 |
40 |
41 | if result.title.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil {
42 | return false
43 | }
44 |
45 | if result.subtitle.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil {
46 | return false
47 | }
48 |
49 | return true
50 | }
51 | }
52 | }
53 |
54 | extension MKLocalSearchCompletion: Identifiable {}
55 |
--------------------------------------------------------------------------------
/sunshine/SearchBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchBar.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 9/24/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | /**
12 | This search bar element is based on https://www.mozzafiller.com/posts/mklocalsearchcompleter-swiftui-combine
13 | */
14 |
15 | struct SearchBar: UIViewRepresentable {
16 | @Binding var text: String
17 | @Binding var toggled: Bool
18 |
19 | class Coordinator: NSObject, UISearchBarDelegate {
20 |
21 | @Binding var text: String
22 | @Binding var toggled: Bool
23 |
24 | init(text: Binding, toggled: Binding) {
25 | _text = text
26 | _toggled = toggled
27 | }
28 |
29 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
30 | text = searchText
31 |
32 | }
33 |
34 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
35 | searchBar.setShowsCancelButton(true, animated: true)
36 | toggled = true
37 | }
38 |
39 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
40 | searchBar.resignFirstResponder()
41 | searchBar.setShowsCancelButton(false, animated: true)
42 | toggled = false
43 | }
44 |
45 | }
46 |
47 | func makeCoordinator() -> SearchBar.Coordinator {
48 | return Coordinator(text: $text, toggled: $toggled)
49 | }
50 |
51 | func makeUIView(context: UIViewRepresentableContext) -> UISearchBar {
52 | let searchBar = UISearchBar(frame: .zero)
53 | searchBar.delegate = context.coordinator
54 | searchBar.searchBarStyle = .minimal
55 | searchBar.placeholder = "Enter a city name e.g. New York"
56 | searchBar.setShowsCancelButton(true, animated: true)
57 | searchBar.becomeFirstResponder()
58 | return searchBar
59 | }
60 |
61 | func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) {
62 | uiView.text = text
63 | }
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/sunshine/Persistence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Persistence.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 11/26/20.
6 | //
7 |
8 | import CoreData
9 |
10 | struct PersistenceController {
11 | static let shared = PersistenceController()
12 |
13 | static var preview: PersistenceController = {
14 | let result = PersistenceController(inMemory: true)
15 | let viewContext = result.container.viewContext
16 | for _ in 0..<10 {
17 | let newItem = Item(context: viewContext)
18 | newItem.timestamp = Date()
19 | }
20 | do {
21 | try viewContext.save()
22 | } catch {
23 | // Replace this implementation with code to handle the error appropriately.
24 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
25 | let nsError = error as NSError
26 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
27 | }
28 | return result
29 | }()
30 |
31 | let container: NSPersistentContainer
32 |
33 | init(inMemory: Bool = false) {
34 | container = NSPersistentContainer(name: "sunshine")
35 | if inMemory {
36 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
37 | }
38 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in
39 | if let error = error as NSError? {
40 | // Replace this implementation with code to handle the error appropriately.
41 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
42 |
43 | /*
44 | Typical reasons for an error here include:
45 | * The parent directory does not exist, cannot be created, or disallows writing.
46 | * The persistent store is not accessible, due to permissions or data protection when the device is locked.
47 | * The device is out of space.
48 | * The store could not be migrated to the current model version.
49 | Check the error message to determine what the actual problem was.
50 | */
51 | fatalError("Unresolved error \(error), \(error.userInfo)")
52 | }
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/sunshine/WeatherAPIClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Store.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 11/26/20.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | enum ViewState: String{
12 | case loading
13 | case error
14 | case idle
15 | }
16 |
17 | enum Temperature: Int{
18 | case fahrenheit = 0
19 | case celsius = 1
20 | case kelvin = 2
21 | }
22 |
23 | enum Speed: Int{
24 | case mph = 0
25 | case kmh = 1
26 | case mps = 2
27 | }
28 |
29 | enum MainDaypart: String{
30 | case dawn
31 | case mid
32 | case dusk
33 | case night
34 | }
35 |
36 | enum MainWeatherType: String, Decodable{
37 | case Thunderstorm
38 | case Drizzle
39 | case Rain
40 | case Snow
41 | case Clear
42 | case Few_Clouds
43 | case Clouds
44 | case Mist
45 | case Haze
46 | case Fog
47 | case Smoke
48 | }
49 |
50 | struct WeatherObject: Decodable {
51 | let id: Int
52 | let main: MainWeatherType
53 | let description: String
54 | }
55 |
56 | struct CurrentForecast: Decodable {
57 | let dt: Int
58 | let sunrise: Int
59 | let sunset: Int
60 | let temp: Double
61 | let feelsLike: Double
62 | let pressure: Int
63 | let humidity: Int
64 | let dewPoint: Double
65 | let clouds: Int
66 | let visibility: Int
67 | let windSpeed: Double
68 | let windDeg: Int
69 | let weather: [WeatherObject]
70 | }
71 |
72 | struct HourlyForecast: Hashable, Decodable {
73 |
74 | static func == (lhs: HourlyForecast, rhs: HourlyForecast) -> Bool {
75 | return lhs.dt > rhs.dt
76 | }
77 |
78 | func hash(into hasher: inout Hasher) {
79 | hasher.combine(dt)
80 | }
81 |
82 | let dt: Int
83 | let temp: Double
84 | let feelsLike: Double
85 | let pressure: Int
86 | let humidity: Int
87 | let dewPoint: Double
88 | let clouds: Int
89 | let visibility: Int
90 | let windSpeed: Double
91 | let weather: [WeatherObject]
92 | }
93 |
94 | struct DetailTemp: Decodable {
95 | let day: Double
96 | let min: Double
97 | let max: Double
98 | let night: Double
99 | let eve: Double
100 | let morn: Double
101 | }
102 |
103 | struct DetailFeelsLikeTemp: Decodable {
104 | let day: Double
105 | let night: Double
106 | let eve: Double
107 | let morn: Double
108 | }
109 |
110 | struct DailyForecast: Hashable, Decodable {
111 | static func == (lhs: DailyForecast, rhs: DailyForecast) -> Bool {
112 | return lhs.dt > rhs.dt
113 | }
114 |
115 | func hash(into hasher: inout Hasher) {
116 | hasher.combine(dt)
117 | }
118 |
119 | let dt: Int
120 | let sunrise: Int
121 | let sunset: Int
122 | let temp: DetailTemp
123 | let feelsLike: DetailFeelsLikeTemp
124 | let pressure: Int
125 | let humidity: Int
126 | let dewPoint: Double
127 | let windSpeed: Double
128 | let windDeg: Int
129 | let weather: [WeatherObject]
130 | let clouds: Int
131 | }
132 |
133 | struct Response: Decodable {
134 | let lat: Double
135 | let lon: Double
136 | let timezone: String
137 | let timezoneOffset: Int
138 | let current: CurrentForecast
139 | let hourly: [HourlyForecast]
140 | let daily: [DailyForecast]
141 |
142 | }
143 |
144 | class WeatherAPIClient {
145 | private let apiKey = ""// OPEN WEATHER MAP TOKEN GOES HERE
146 | private let decoder = JSONDecoder()
147 | private let session: URLSession
148 |
149 | init(configuration: URLSessionConfiguration) {
150 | self.session = URLSession(configuration: configuration)
151 | }
152 |
153 | convenience init() {
154 | self.init(configuration: .default)
155 | }
156 |
157 | func fetch (lat: Double, lon: Double, handler: @escaping (Result) -> Void) {
158 | let baseURL = URL(string: "https://api.openweathermap.org/data/2.5/onecall?lat=\(lat)&lon=\(lon)&appid=\(apiKey)&exclude=minutely")
159 | // Ensure the URL is unpacked
160 | guard let url = baseURL else {
161 | print("error...")
162 | return
163 | }
164 |
165 | let request = URLRequest(url: url)
166 |
167 | // Fetch data with URL session
168 | session.dataTask(with: request) {(data, response, error) in
169 | // If an error occurs, call the callback with the error
170 | if let error = error {
171 | handler(.failure(error))
172 | } else {
173 | // If there's no error try to decode the JSON response
174 | // The following is equivalent to a try/catch block in JS
175 | do {
176 | // Convert from snake case to camel case
177 | self.decoder.keyDecodingStrategy = .convertFromSnakeCase
178 | let data = data ?? Data()
179 | let response = try self.decoder.decode(Response.self, from: data)
180 | // Call the callback with the response
181 | handler(.success(response))
182 | } catch {
183 | // Call the callback with an error if decoding the JSON fails
184 | handler(.failure(error))
185 | }
186 | }
187 |
188 | }.resume()
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/sunshine/SunDial.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SunDial.swift
3 | // pagetest
4 | //
5 | // Created by Maxime on 9/14/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct Arc: Shape {
11 | var startAngle: Angle = .degrees(0)
12 | var endAngle: Angle = .degrees(180)
13 | var clockWise: Bool = true
14 |
15 | func path(in rect: CGRect) -> Path {
16 | var path = Path()
17 |
18 | let center = CGPoint(x: rect.midX, y: rect.maxY)
19 | let radius = CGFloat(130)
20 |
21 | path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockWise)
22 |
23 | return path
24 | }
25 | }
26 | //
27 | //struct XAxis: Shape {
28 | // func path(in rect: CGRect) -> Path {
29 | // var path = Path()
30 | //
31 | // let center = CGPoint(x: rect.midX, y: rect.maxY)
32 | //
33 | // path.move(to: center)
34 | // path.addLine(to: CGPoint(x: rect.minX, y: center.y))
35 | // path.addLine(to: CGPoint(x: rect.maxX, y: center.y))
36 | //
37 | //
38 | // return path
39 | // }
40 | //}
41 |
42 | struct SunDial: View {
43 | var sunrise: Int
44 | var sunset: Int
45 | var timestamp: Int
46 | var timezone: String
47 |
48 | let strokeDash: [CGFloat] = [5,5]
49 |
50 | @State private var targetAngle: Double = 0
51 |
52 | // let timer = Timer.publish(every: 600, on: .main, in: .common).autoconnect()
53 | var date: Date { Date(timeIntervalSince1970: Double(timestamp))}
54 |
55 |
56 | var nextAngle: Double {
57 | updateTargetAngle(date)
58 | }
59 |
60 | var body: some View {
61 | return
62 | ZStack {
63 | GeometryReader { geometry in
64 | VStack(spacing: 15) {
65 | Text("Sun from \(formatDateToHoursMinutes(sunrise)) to \(formatDateToHoursMinutes(sunset))")
66 | .font(.system(size: 14, weight: .semibold))
67 | .foregroundColor(.black)
68 | .padding(.top, 15)
69 | ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
70 | Arc()
71 | .fill(LinearGradient(
72 | gradient: Gradient(stops: [
73 | .init(color: Color(#colorLiteral(red: 1, green: 0.892666757106781, blue: 0.6166666746139526, alpha: 1)), location: 0),
74 | .init(color: Color(#colorLiteral(red: 1, green: 0.6822500228881836, blue: 0.612500011920929, alpha: 0.30000001192092896)), location: 1)]),
75 | startPoint: UnitPoint(x: 0.5, y: 6.905165726926942e-15),
76 | endPoint: UnitPoint(x: 0.49999995240056083, y: 0.9718156807337223)))
77 | // .stroke(Color.yellow, style: StrokeStyle(lineWidth: 2, dash: strokeDash))
78 |
79 | .frame(height: 130)
80 | .opacity(0.7)
81 | // XAxis()
82 | // .frame(width: 300, height: 5)
83 | HStack(spacing: 245) {
84 | VStack {
85 | Circle()
86 | .fill(Color.yellow)
87 | .frame(width: 10, height: 10)
88 | }
89 | VStack {
90 | Circle()
91 | .fill(Color.yellow)
92 | .frame(width: 10, height: 10)
93 |
94 | }
95 | }.padding(.bottom, 0)
96 | VStack {
97 | if (Int(date.timeIntervalSince1970) >= sunrise && Int(date.timeIntervalSince1970) < sunset) {
98 | Image("sun-mid")
99 | .resizable()
100 | .rotationEffect(.degrees(-self.targetAngle))
101 | .scaledToFit()
102 | .frame(height: 50)
103 | .position(x: 0, y: 0)
104 | .rotationEffect( .degrees(self.targetAngle))
105 | .animation(Animation.easeInOut(duration: 3 + 3 * self.targetAngle / 180))
106 | }
107 |
108 | }
109 | .frame(width: 260, height: 1)
110 | }
111 | }
112 | }
113 | .frame(height: 175)
114 | .clipped()
115 | .padding(.leading,10)
116 | .padding(.trailing,10)
117 | .animation(Animation.easeOut(duration: 0.7).delay(0))
118 | // On timer update
119 | // .onReceive(timer){time in
120 | // self.targetAngle = updateTargetAngle(time)
121 | // }
122 | // On prop change
123 | .onChange(of: nextAngle) { angle in
124 | self.targetAngle = angle
125 | }
126 | // On mount
127 | .onAppear {
128 | // DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
129 | self.targetAngle = updateTargetAngle(date)
130 | // }
131 | }
132 | }
133 | }
134 |
135 | func updateTargetAngle(_ updatedDate: Date) -> Double {
136 | return Double((Int(updatedDate.timeIntervalSince1970) - sunrise) * 180 / (sunset - sunrise))
137 | }
138 |
139 | func formatDateToHoursMinutes(_ timestamp: Int) -> String {
140 | let date = Date(timeIntervalSince1970: Double(timestamp))
141 | let formatter = DateFormatter()
142 | formatter.timeStyle = .short
143 | formatter.locale = NSLocale.current
144 | formatter.timeZone = TimeZone(identifier: timezone)
145 | var formattedDate:String {formatter.string(from: date)}
146 |
147 | return formattedDate
148 | }
149 |
150 | func str(hour: Int, minutes: Int) -> Double {
151 | return Double(hour) + Double(minutes) / 60
152 | }
153 | }
154 |
155 | struct SunDial_Previews: PreviewProvider {
156 | static var previews: some View {
157 | SunDial(sunrise: 1600374388, sunset: 1600418714, timestamp: 1600394388, timezone: "Asia/Tokyo")
158 | }
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/sunshine/Settings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Settings.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 9/20/20.
6 | //
7 |
8 | import SwiftUI
9 | import MapKit
10 |
11 | enum Section: Int{
12 | case locations = 0
13 | case settings = 1
14 | }
15 |
16 | struct BackgroundClearView: UIViewRepresentable {
17 | func makeUIView(context: Context) -> UIView {
18 | let view = UIView()
19 | DispatchQueue.main.async {
20 | view.superview?.superview?.backgroundColor = .clear
21 | }
22 | return view
23 | }
24 |
25 | func updateUIView(_ uiView: UIView, context: Context) {}
26 | }
27 |
28 | struct SettingsView: View {
29 | @EnvironmentObject var viewModel: ViewModel
30 |
31 | var body: some View {
32 | VStack {
33 | VStack {
34 | HStack {
35 | Text("Temperature")
36 | .font(.system(size: 18, weight: .medium))
37 | .foregroundColor(.secondary)
38 | Spacer()
39 | }
40 | Picker(selection: $viewModel.temperature, label: Text("Temperature"), content: {
41 | Text("Fahrenheit °F 🇺🇸").tag(Temperature.fahrenheit)
42 | Text("Celsius °C 🌍").tag(Temperature.celsius)
43 | Text("Kelvin °K 🤓").tag(Temperature.kelvin)
44 | })
45 | .pickerStyle(SegmentedPickerStyle())
46 | }
47 | .padding(.top, 20)
48 | VStack {
49 | HStack {
50 | Text("Wind Speed")
51 | .font(.system(size: 18, weight: .medium))
52 | .foregroundColor(.secondary)
53 | Spacer()
54 | }
55 | Picker(selection: $viewModel.speed, label: Text("Speed"), content: {
56 | Text("mph 🇺🇸").tag(Speed.mph)
57 | Text("km/h 🌍").tag(Speed.kmh)
58 | Text("m/s 🤓").tag(Speed.mps)
59 | })
60 | .pickerStyle(SegmentedPickerStyle())
61 | }
62 | .padding(.top, 20)
63 | }
64 | .padding([.leading, .trailing], 12)
65 | }
66 | }
67 |
68 | struct SheetPane: View {
69 | @Environment(\.managedObjectContext) private var viewContext
70 | @EnvironmentObject var viewModel: ViewModel
71 |
72 | @ObservedObject var locationSearchService: LocationSearchService
73 | var savedLocations: FetchedResults
74 |
75 | @State private var isSearchToggled = false
76 | @State private var selection = Section.locations
77 |
78 | var currentTitles: [String] {
79 | savedLocations.map { $0.title! }
80 | }
81 |
82 | var body: some View {
83 | ZStack {
84 | VisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial)).ignoresSafeArea(.all)
85 | VStack {
86 | Picker(selection: $selection, label: Text(""), content: {
87 | Text("Locations").tag(Section.locations)
88 | Text("Settings").tag(Section.settings)
89 |
90 | })
91 | .pickerStyle(SegmentedPickerStyle())
92 | .padding([.leading, .trailing], 100)
93 |
94 | if(selection == Section.settings) {
95 | GeometryReader { _ in
96 | SettingsView()
97 | }
98 | }
99 |
100 | if(selection == Section.locations) {
101 | GeometryReader { geometry in
102 | // If Search is not toggled: show the list of saved locations
103 | if (!isSearchToggled) {
104 | VStack {
105 | VStack {
106 | HStack {
107 | Text("Your saved locations (\(savedLocations.count)/10)")
108 | .font(.system(size: 18, weight: .medium))
109 | .foregroundColor(.secondary)
110 | Spacer()
111 | }
112 | .padding([.leading, .trailing], 12)
113 | .padding(.top, 20)
114 | .padding(.bottom, 10)
115 |
116 | List {
117 | ForEach(savedLocations) { item in
118 | Text(item.name ?? "")
119 | }
120 | .onDelete(perform: removeLocation)
121 | .listRowBackground(Color.clear)
122 | }
123 | }
124 | Button(action: {
125 | self.isSearchToggled = true
126 | }) {
127 | Image(systemName: "magnifyingglass")
128 | .font(.system(size: 18, weight: .medium))
129 | Text("Add locations")
130 | }
131 | .disabled(savedLocations.count >= 10)
132 | .padding(.bottom, 10)
133 | }
134 | // If search is toggled: show the search locations view
135 | } else {
136 | VStack {
137 | SearchBar(text: $locationSearchService.searchQuery, toggled: $isSearchToggled)
138 | // If there are no items in the results of the search query (completion) AND if the search query is not empty, show the empty state
139 | if (locationSearchService.completions.count == 0 && locationSearchService.searchQuery != "" ) {
140 | Spacer()
141 | Text("No results found.")
142 | .font(.system(size: 14, weight: .medium))
143 | .foregroundColor(.secondary)
144 | Spacer()
145 | // Else show the list
146 | } else {
147 | List {
148 | ForEach(locationSearchService.completions, id: \.self) { completion in
149 | VStack(alignment: .leading) {
150 | HStack {
151 | Text(completion.title)
152 | // If this completion is not yet added in the locations array, add the Add Location button
153 | if (!currentTitles.contains(completion.title)) {
154 | Spacer()
155 | Button(action: {
156 | addLocation(completion)
157 | }) {
158 | Image(systemName: "plus")
159 | }
160 | // Else show a checkmark to tell the user this view has already been added
161 | } else {
162 | Spacer()
163 | Image(systemName: "checkmark")
164 |
165 | }
166 | }
167 | }
168 | .padding([.leading, .trailing], 10)
169 | }
170 | .listRowBackground(Color.clear)
171 | }
172 | }
173 | }
174 | }
175 | }
176 | }
177 | }
178 | .padding([.top], 20)
179 | }
180 | .onAppear {
181 | UITableView.appearance().backgroundColor = UIColor.clear
182 | UITableViewCell.appearance().backgroundColor = UIColor.clear
183 | }
184 | .background(BackgroundClearView())
185 | }
186 |
187 | func addLocation(_ completion: MKLocalSearchCompletion) {
188 | let searchRequest = MKLocalSearch.Request(completion: completion)
189 | let search = MKLocalSearch(request: searchRequest)
190 |
191 | search.start { response, error in
192 | guard let coordinate = response?.mapItems[0].placemark.coordinate else {
193 | return
194 | }
195 |
196 | guard let name = response?.mapItems[0].name else {
197 | return
198 | }
199 |
200 | let lat = coordinate.latitude
201 | let lon = coordinate.longitude
202 |
203 | let newSavedLocation = Location(context: viewContext)
204 | newSavedLocation.name = name
205 | newSavedLocation.title = completion.title
206 | newSavedLocation.lat = lat
207 | newSavedLocation.lon = lon
208 | newSavedLocation.id = UUID()
209 | newSavedLocation.timestamp = Date()
210 |
211 | do {
212 | try viewContext.save()
213 | } catch {
214 | // Replace this implementation with code to handle the error appropriately.
215 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
216 | let nsError = error as NSError
217 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
218 | }
219 | }
220 | }
221 |
222 | func removeLocation(at offsets: IndexSet) {
223 | viewModel.items.remove(atOffsets: offsets)
224 | offsets.map { savedLocations[$0] }.forEach(viewContext.delete)
225 |
226 | do {
227 | try viewContext.save()
228 | } catch {
229 | // Replace this implementation with code to handle the error appropriately.
230 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
231 | let nsError = error as NSError
232 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
233 | }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/sunshine/Forecast.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Forecast.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 11/25/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct WeatherImage: View {
11 | var weather: WeatherObject
12 | var daypart: MainDaypart
13 |
14 | var body: some View {
15 | ZStack {
16 | if(weather.id == 800) {
17 | if (daypart == MainDaypart.night) {
18 | Image("moon")
19 | .resizable()
20 | .scaledToFit()
21 | }
22 |
23 | if (daypart == MainDaypart.mid) {
24 | Image("sun-mid")
25 | .resizable()
26 | .scaledToFit()
27 | }
28 |
29 | if(daypart == MainDaypart.dawn || daypart == MainDaypart.dusk) {
30 | Image("sun-dawn")
31 | .resizable()
32 | .scaledToFit()
33 | }
34 | }
35 |
36 | if (weather.id == 801 || weather.id == 802) {
37 | Image("clouds")
38 | .resizable()
39 | .scaledToFit()
40 | }
41 |
42 | if (weather.id > 802) {
43 | Image("big-cloud")
44 | .resizable()
45 | .scaledToFit()
46 | }
47 |
48 | if (weather.id >= 500 && weather.id < 600) {
49 | if (weather.id <= 501) {
50 | Image("light-rain-drops")
51 | .resizable()
52 | .scaledToFit()
53 | } else {
54 | Image("heavy-rain-drops")
55 | .resizable()
56 | .scaledToFit()
57 | }
58 | }
59 |
60 | if (weather.id >= 300 && weather.id < 400) {
61 | if (weather.id <= 311) {
62 | Image("light-rain-drops")
63 | .resizable()
64 | .scaledToFit()
65 | } else {
66 | Image("heavy-rain-drops")
67 | .resizable()
68 | .scaledToFit()
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | struct HourlyCard: View {
76 | var forecast: HourlyForecast
77 | var timezone: String
78 | var sunrise: Int
79 | var sunset: Int
80 | var nextDaySunrise: Int
81 | var tempUnit: Temperature
82 |
83 |
84 | var currentDayPart: MainDaypart {
85 | return getDayPart(sunrise: Double(sunrise), sunset: Double(sunset), nextDaySunrise: Double(nextDaySunrise), timestamp: Double(forecast.dt))
86 | }
87 |
88 | var body: some View {
89 | ZStack {
90 | RoundedRectangle(cornerRadius: 15)
91 | .fill(LinearGradient(
92 | gradient: Gradient(colors: gradients[currentDayPart]!),
93 | startPoint: UnitPoint(x: 0.5, y: 0),
94 | endPoint: UnitPoint(x: 0.5, y: 0.6)))
95 | .frame(width: 60, height: 120)
96 | .shadow(color: Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.25)), radius:5, x:0, y:4)
97 | if (isCloudy(forecast.weather[0].id)) {
98 | if (currentDayPart == MainDaypart.night) {
99 | RoundedRectangle(cornerRadius: 15)
100 | .fill(LinearGradient(
101 | gradient: Gradient(stops: [
102 | .init(color: Color(#colorLiteral(red: 0.09019608050584793, green: 0.1411764770746231, blue: 0.16862745583057404, alpha: 1)), location: 0),
103 | .init(color: Color(#colorLiteral(red: 0.250980406999588, green: 0.2705882489681244, blue: 0.3294117748737335, alpha: 1)), location: 1)]),
104 | startPoint: UnitPoint(x: 0, y: 0.5),
105 | endPoint: UnitPoint(x: 1.2, y: 0.5)))
106 | .opacity(0.7)
107 | .frame(width: 60, height: 120)
108 | } else {
109 | RoundedRectangle(cornerRadius: 15)
110 | .fill(LinearGradient(
111 | gradient: Gradient(stops: [
112 | .init(color: Color(#colorLiteral(red: 0.6976562142, green: 0.7035937309, blue: 0.7124999762, alpha: 0.700952492)), location: 0),
113 | .init(color: Color(#colorLiteral(red: 0.8545138835906982, green: 0.8718518614768982, blue: 0.8916666507720947, alpha: 0.20000000298023224)), location: 1)]),
114 | startPoint: UnitPoint(x: 0.5, y: 0),
115 | endPoint: UnitPoint(x: 0.5, y: 0.5)))
116 | .opacity(0.7)
117 | .frame(width: 60, height: 120)
118 | }
119 | }
120 | VStack {
121 | FormattedTemperature(temperature: forecast.temp, unit: tempUnit)
122 | .font(.system(size: 14, weight: .medium))
123 | .foregroundColor(.white)
124 | .padding(.top, 10)
125 | WeatherImage(weather: forecast.weather[0], daypart: currentDayPart)
126 | .frame(width: 50, height: 50)
127 | ZStack {
128 | VisualEffectView(effect: UIBlurEffect(style: .light))
129 | Text("\(formatToLocalTime(forecast.dt, timezone))")
130 | .font(.system(size: 11, weight: .medium))
131 | .foregroundColor(.black)
132 | }
133 | .frame(height: 25, alignment: .bottom)
134 | }
135 | .cornerRadius(15)
136 | .frame(height: 120)
137 | .clipped()
138 | }
139 | .frame(height: 140)
140 | }
141 |
142 | func formatToLocalTime(_ timestamp: Int, _ timezone: String) -> String {
143 | let date = Date(timeIntervalSince1970: Double(timestamp))
144 | let formatter = DateFormatter()
145 | formatter.timeStyle = .short
146 | formatter.locale = NSLocale.current
147 | formatter.timeZone = TimeZone(identifier: timezone)
148 | var formattedDate:String {formatter.string(from: date)}
149 |
150 | return formattedDate
151 | }
152 | }
153 |
154 | struct NextSixHours: View {
155 | var hourly: [HourlyForecast]
156 | var timezone: String
157 | var sunrise: Int
158 | var sunset: Int
159 | var nextDaySunrise: Int
160 | var tempUnit: Temperature
161 |
162 | var body: some View {
163 | ScrollView(.horizontal, showsIndicators: false) {
164 | HStack(spacing: 0) {
165 | ForEach(hourly.prefix(6), id: \.dt) { forecast in
166 | HourlyCard(forecast: forecast, timezone:timezone, sunrise: sunrise, sunset: sunset, nextDaySunrise: nextDaySunrise, tempUnit: tempUnit)
167 | .padding(.leading, 10)
168 | }
169 | }
170 | }
171 | }
172 | }
173 |
174 | struct DailyCard: View {
175 | var forecast: DailyForecast
176 | var tempUnit: Temperature
177 |
178 | let height: CGFloat = 70
179 |
180 | let formatter: DateFormatter = {
181 | let formatter = DateFormatter()
182 | formatter.dateFormat = "E"
183 | return formatter
184 | }()
185 |
186 | var body: some View {
187 | ZStack {
188 | RoundedRectangle(cornerRadius: 20)
189 | .fill(LinearGradient(
190 | gradient: Gradient(colors: gradients[MainDaypart.mid]!),
191 | startPoint: UnitPoint(x: 0, y: 0.5),
192 | endPoint: UnitPoint(x: 1.2, y: 0.5)))
193 | .frame(height: height)
194 | .shadow(color: Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.25)), radius:5, x:0, y:4)
195 | if (isCloudy(forecast.weather[0].id)) {
196 | RoundedRectangle(cornerRadius: 20)
197 | .fill(LinearGradient(
198 | gradient: Gradient(stops: [
199 | .init(color: Color(#colorLiteral(red: 0.6976562142, green: 0.7035937309, blue: 0.7124999762, alpha: 0.700952492)), location: 0),
200 | .init(color: Color(#colorLiteral(red: 0.8545138835906982, green: 0.8718518614768982, blue: 0.8916666507720947, alpha: 0.20000000298023224)), location: 1)]),
201 | startPoint: UnitPoint(x: 0, y: 0.5),
202 | endPoint: UnitPoint(x: 1.2, y: 0.5)))
203 | .opacity(0.7)
204 | .frame(height: height)
205 | }
206 | HStack {
207 | Text(Date(timeIntervalSince1970: Double(forecast.dt)), formatter: formatter)
208 | .font(.system(size: 18, weight: .medium))
209 | .foregroundColor(.white)
210 | Spacer()
211 | HStack(alignment: .bottom) {
212 | FormattedTemperature(temperature: forecast.temp.max, unit: tempUnit)
213 | .font(.system(size: 18, weight: .medium))
214 | .foregroundColor(.white)
215 | FormattedTemperature(temperature: forecast.temp.min, unit: tempUnit)
216 | .font(.system(size: 14, weight: .medium))
217 | .foregroundColor(.white)
218 | }
219 | HStack {
220 | Text("\(forecast.weather[0].main.rawValue)")
221 | .font(.system(size: 14, weight: .medium))
222 | .foregroundColor(.white)
223 | WeatherImage(weather: forecast.weather[0], daypart: MainDaypart.mid)
224 | .frame(width: 50, height: 50)
225 | }.frame(width: 150, alignment: .trailing)
226 |
227 | }
228 | .padding([.leading, .trailing], 10)
229 | }
230 | }
231 | }
232 |
233 | struct NextSevenDays: View {
234 | var daily: [DailyForecast]
235 | var tempUnit: Temperature
236 |
237 | var body: some View {
238 | VStack {
239 | ForEach(daily.suffix(7), id: \.dt) { forecast in
240 | DailyCard(forecast: forecast, tempUnit: tempUnit)
241 | .padding([.leading, .trailing], 10)
242 | .padding([.bottom], 5)
243 | }
244 | }
245 | }
246 | }
247 |
248 | struct ForecastView: View {
249 | @EnvironmentObject var viewModel: ViewModel
250 |
251 | var body: some View {
252 | VStack {
253 | HStack {
254 | Text("Forecast").font(.system(size: 24, weight: .medium))
255 | Spacer()
256 | }
257 | .padding([.leading, .trailing], 20)
258 | .padding(.top, 5)
259 | .padding(.bottom, 10)
260 |
261 | if(viewModel.state == ViewState.loading) {
262 | Text("loading")
263 | }
264 |
265 | if(viewModel.state == ViewState.idle && viewModel.focusedItem != nil){
266 | ScrollView {
267 | VStack {
268 | HStack {
269 | Text("Next 6 Hours").font(.system(size: 18, weight: .medium))
270 | Spacer()
271 | }
272 | .padding([.leading, .trailing], 20)
273 | NextSixHours(hourly: viewModel.focusedItem!.hourly, timezone: viewModel.focusedItem!.timezone, sunrise: viewModel.focusedItem!.current.sunrise, sunset: viewModel.focusedItem!.current.sunset, nextDaySunrise: viewModel.focusedItem!.daily[1].sunrise, tempUnit: viewModel.temperature)
274 | HStack {
275 | Text("Next 7 Days").font(.system(size: 18, weight: .medium))
276 | Spacer()
277 | }
278 | .padding([.top, .leading, .trailing], 20)
279 | NextSevenDays(daily: viewModel.focusedItem!.daily, tempUnit: viewModel.temperature)
280 | }
281 | }
282 | .padding(.bottom, 30)
283 | }
284 | }
285 | }
286 | }
287 |
288 |
--------------------------------------------------------------------------------
/sunshine.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 9C7992A425701B180072BEE2 /* sunshineApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992A325701B180072BEE2 /* sunshineApp.swift */; };
11 | 9C7992A625701B180072BEE2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992A525701B180072BEE2 /* ContentView.swift */; };
12 | 9C7992A825701B1D0072BEE2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C7992A725701B1D0072BEE2 /* Assets.xcassets */; };
13 | 9C7992AB25701B1D0072BEE2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C7992AA25701B1D0072BEE2 /* Preview Assets.xcassets */; };
14 | 9C7992AD25701B1D0072BEE2 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992AC25701B1D0072BEE2 /* Persistence.swift */; };
15 | 9C7992B025701B1D0072BEE2 /* sunshine.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992AE25701B1D0072BEE2 /* sunshine.xcdatamodeld */; };
16 | 9C7992BB25701B1D0072BEE2 /* sunshineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992BA25701B1D0072BEE2 /* sunshineTests.swift */; };
17 | 9C7992C625701B1D0072BEE2 /* sunshineUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992C525701B1D0072BEE2 /* sunshineUITests.swift */; };
18 | 9C7992D725701C770072BEE2 /* WeatherAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992D625701C770072BEE2 /* WeatherAPIClient.swift */; };
19 | 9C7992DC25701CC10072BEE2 /* SunDial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992DB25701CC10072BEE2 /* SunDial.swift */; };
20 | 9C7992E125701CDB0072BEE2 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992E025701CDB0072BEE2 /* Settings.swift */; };
21 | 9C7992E625701D170072BEE2 /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992E525701D170072BEE2 /* Forecast.swift */; };
22 | 9C7992EB25701D530072BEE2 /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992EA25701D530072BEE2 /* LocationService.swift */; };
23 | 9C7992F025701D8A0072BEE2 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7992EF25701D8A0072BEE2 /* SearchBar.swift */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXContainerItemProxy section */
27 | 9C7992B725701B1D0072BEE2 /* PBXContainerItemProxy */ = {
28 | isa = PBXContainerItemProxy;
29 | containerPortal = 9C79929825701B180072BEE2 /* Project object */;
30 | proxyType = 1;
31 | remoteGlobalIDString = 9C79929F25701B180072BEE2;
32 | remoteInfo = sunshine;
33 | };
34 | 9C7992C225701B1D0072BEE2 /* PBXContainerItemProxy */ = {
35 | isa = PBXContainerItemProxy;
36 | containerPortal = 9C79929825701B180072BEE2 /* Project object */;
37 | proxyType = 1;
38 | remoteGlobalIDString = 9C79929F25701B180072BEE2;
39 | remoteInfo = sunshine;
40 | };
41 | /* End PBXContainerItemProxy section */
42 |
43 | /* Begin PBXFileReference section */
44 | 9C7992A025701B180072BEE2 /* sunshine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sunshine.app; sourceTree = BUILT_PRODUCTS_DIR; };
45 | 9C7992A325701B180072BEE2 /* sunshineApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sunshineApp.swift; sourceTree = ""; };
46 | 9C7992A525701B180072BEE2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
47 | 9C7992A725701B1D0072BEE2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
48 | 9C7992AA25701B1D0072BEE2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
49 | 9C7992AC25701B1D0072BEE2 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; };
50 | 9C7992AF25701B1D0072BEE2 /* sunshine.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = sunshine.xcdatamodel; sourceTree = ""; };
51 | 9C7992B125701B1D0072BEE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
52 | 9C7992B625701B1D0072BEE2 /* sunshineTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = sunshineTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
53 | 9C7992BA25701B1D0072BEE2 /* sunshineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sunshineTests.swift; sourceTree = ""; };
54 | 9C7992BC25701B1D0072BEE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
55 | 9C7992C125701B1D0072BEE2 /* sunshineUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = sunshineUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56 | 9C7992C525701B1D0072BEE2 /* sunshineUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sunshineUITests.swift; sourceTree = ""; };
57 | 9C7992C725701B1D0072BEE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
58 | 9C7992D625701C770072BEE2 /* WeatherAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPIClient.swift; sourceTree = ""; };
59 | 9C7992DB25701CC10072BEE2 /* SunDial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunDial.swift; sourceTree = ""; };
60 | 9C7992E025701CDB0072BEE2 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; };
61 | 9C7992E525701D170072BEE2 /* Forecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Forecast.swift; sourceTree = ""; };
62 | 9C7992EA25701D530072BEE2 /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = ""; };
63 | 9C7992EF25701D8A0072BEE2 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; };
64 | /* End PBXFileReference section */
65 |
66 | /* Begin PBXFrameworksBuildPhase section */
67 | 9C79929D25701B180072BEE2 /* Frameworks */ = {
68 | isa = PBXFrameworksBuildPhase;
69 | buildActionMask = 2147483647;
70 | files = (
71 | );
72 | runOnlyForDeploymentPostprocessing = 0;
73 | };
74 | 9C7992B325701B1D0072BEE2 /* Frameworks */ = {
75 | isa = PBXFrameworksBuildPhase;
76 | buildActionMask = 2147483647;
77 | files = (
78 | );
79 | runOnlyForDeploymentPostprocessing = 0;
80 | };
81 | 9C7992BE25701B1D0072BEE2 /* Frameworks */ = {
82 | isa = PBXFrameworksBuildPhase;
83 | buildActionMask = 2147483647;
84 | files = (
85 | );
86 | runOnlyForDeploymentPostprocessing = 0;
87 | };
88 | /* End PBXFrameworksBuildPhase section */
89 |
90 | /* Begin PBXGroup section */
91 | 9C79929725701B180072BEE2 = {
92 | isa = PBXGroup;
93 | children = (
94 | 9C7992A225701B180072BEE2 /* sunshine */,
95 | 9C7992B925701B1D0072BEE2 /* sunshineTests */,
96 | 9C7992C425701B1D0072BEE2 /* sunshineUITests */,
97 | 9C7992A125701B180072BEE2 /* Products */,
98 | );
99 | sourceTree = "";
100 | };
101 | 9C7992A125701B180072BEE2 /* Products */ = {
102 | isa = PBXGroup;
103 | children = (
104 | 9C7992A025701B180072BEE2 /* sunshine.app */,
105 | 9C7992B625701B1D0072BEE2 /* sunshineTests.xctest */,
106 | 9C7992C125701B1D0072BEE2 /* sunshineUITests.xctest */,
107 | );
108 | name = Products;
109 | sourceTree = "";
110 | };
111 | 9C7992A225701B180072BEE2 /* sunshine */ = {
112 | isa = PBXGroup;
113 | children = (
114 | 9C7992A325701B180072BEE2 /* sunshineApp.swift */,
115 | 9C7992A525701B180072BEE2 /* ContentView.swift */,
116 | 9C7992DB25701CC10072BEE2 /* SunDial.swift */,
117 | 9C7992E025701CDB0072BEE2 /* Settings.swift */,
118 | 9C7992E525701D170072BEE2 /* Forecast.swift */,
119 | 9C7992EF25701D8A0072BEE2 /* SearchBar.swift */,
120 | 9C7992EA25701D530072BEE2 /* LocationService.swift */,
121 | 9C7992A725701B1D0072BEE2 /* Assets.xcassets */,
122 | 9C7992AC25701B1D0072BEE2 /* Persistence.swift */,
123 | 9C7992B125701B1D0072BEE2 /* Info.plist */,
124 | 9C7992AE25701B1D0072BEE2 /* sunshine.xcdatamodeld */,
125 | 9C7992A925701B1D0072BEE2 /* Preview Content */,
126 | 9C7992D625701C770072BEE2 /* WeatherAPIClient.swift */,
127 | );
128 | path = sunshine;
129 | sourceTree = "";
130 | };
131 | 9C7992A925701B1D0072BEE2 /* Preview Content */ = {
132 | isa = PBXGroup;
133 | children = (
134 | 9C7992AA25701B1D0072BEE2 /* Preview Assets.xcassets */,
135 | );
136 | path = "Preview Content";
137 | sourceTree = "";
138 | };
139 | 9C7992B925701B1D0072BEE2 /* sunshineTests */ = {
140 | isa = PBXGroup;
141 | children = (
142 | 9C7992BA25701B1D0072BEE2 /* sunshineTests.swift */,
143 | 9C7992BC25701B1D0072BEE2 /* Info.plist */,
144 | );
145 | path = sunshineTests;
146 | sourceTree = "";
147 | };
148 | 9C7992C425701B1D0072BEE2 /* sunshineUITests */ = {
149 | isa = PBXGroup;
150 | children = (
151 | 9C7992C525701B1D0072BEE2 /* sunshineUITests.swift */,
152 | 9C7992C725701B1D0072BEE2 /* Info.plist */,
153 | );
154 | path = sunshineUITests;
155 | sourceTree = "";
156 | };
157 | /* End PBXGroup section */
158 |
159 | /* Begin PBXNativeTarget section */
160 | 9C79929F25701B180072BEE2 /* sunshine */ = {
161 | isa = PBXNativeTarget;
162 | buildConfigurationList = 9C7992CA25701B1D0072BEE2 /* Build configuration list for PBXNativeTarget "sunshine" */;
163 | buildPhases = (
164 | 9C79929C25701B180072BEE2 /* Sources */,
165 | 9C79929D25701B180072BEE2 /* Frameworks */,
166 | 9C79929E25701B180072BEE2 /* Resources */,
167 | );
168 | buildRules = (
169 | );
170 | dependencies = (
171 | );
172 | name = sunshine;
173 | productName = sunshine;
174 | productReference = 9C7992A025701B180072BEE2 /* sunshine.app */;
175 | productType = "com.apple.product-type.application";
176 | };
177 | 9C7992B525701B1D0072BEE2 /* sunshineTests */ = {
178 | isa = PBXNativeTarget;
179 | buildConfigurationList = 9C7992CD25701B1D0072BEE2 /* Build configuration list for PBXNativeTarget "sunshineTests" */;
180 | buildPhases = (
181 | 9C7992B225701B1D0072BEE2 /* Sources */,
182 | 9C7992B325701B1D0072BEE2 /* Frameworks */,
183 | 9C7992B425701B1D0072BEE2 /* Resources */,
184 | );
185 | buildRules = (
186 | );
187 | dependencies = (
188 | 9C7992B825701B1D0072BEE2 /* PBXTargetDependency */,
189 | );
190 | name = sunshineTests;
191 | productName = sunshineTests;
192 | productReference = 9C7992B625701B1D0072BEE2 /* sunshineTests.xctest */;
193 | productType = "com.apple.product-type.bundle.unit-test";
194 | };
195 | 9C7992C025701B1D0072BEE2 /* sunshineUITests */ = {
196 | isa = PBXNativeTarget;
197 | buildConfigurationList = 9C7992D025701B1D0072BEE2 /* Build configuration list for PBXNativeTarget "sunshineUITests" */;
198 | buildPhases = (
199 | 9C7992BD25701B1D0072BEE2 /* Sources */,
200 | 9C7992BE25701B1D0072BEE2 /* Frameworks */,
201 | 9C7992BF25701B1D0072BEE2 /* Resources */,
202 | );
203 | buildRules = (
204 | );
205 | dependencies = (
206 | 9C7992C325701B1D0072BEE2 /* PBXTargetDependency */,
207 | );
208 | name = sunshineUITests;
209 | productName = sunshineUITests;
210 | productReference = 9C7992C125701B1D0072BEE2 /* sunshineUITests.xctest */;
211 | productType = "com.apple.product-type.bundle.ui-testing";
212 | };
213 | /* End PBXNativeTarget section */
214 |
215 | /* Begin PBXProject section */
216 | 9C79929825701B180072BEE2 /* Project object */ = {
217 | isa = PBXProject;
218 | attributes = {
219 | LastSwiftUpdateCheck = 1220;
220 | LastUpgradeCheck = 1220;
221 | TargetAttributes = {
222 | 9C79929F25701B180072BEE2 = {
223 | CreatedOnToolsVersion = 12.2;
224 | };
225 | 9C7992B525701B1D0072BEE2 = {
226 | CreatedOnToolsVersion = 12.2;
227 | TestTargetID = 9C79929F25701B180072BEE2;
228 | };
229 | 9C7992C025701B1D0072BEE2 = {
230 | CreatedOnToolsVersion = 12.2;
231 | TestTargetID = 9C79929F25701B180072BEE2;
232 | };
233 | };
234 | };
235 | buildConfigurationList = 9C79929B25701B180072BEE2 /* Build configuration list for PBXProject "sunshine" */;
236 | compatibilityVersion = "Xcode 9.3";
237 | developmentRegion = en;
238 | hasScannedForEncodings = 0;
239 | knownRegions = (
240 | en,
241 | Base,
242 | );
243 | mainGroup = 9C79929725701B180072BEE2;
244 | productRefGroup = 9C7992A125701B180072BEE2 /* Products */;
245 | projectDirPath = "";
246 | projectRoot = "";
247 | targets = (
248 | 9C79929F25701B180072BEE2 /* sunshine */,
249 | 9C7992B525701B1D0072BEE2 /* sunshineTests */,
250 | 9C7992C025701B1D0072BEE2 /* sunshineUITests */,
251 | );
252 | };
253 | /* End PBXProject section */
254 |
255 | /* Begin PBXResourcesBuildPhase section */
256 | 9C79929E25701B180072BEE2 /* Resources */ = {
257 | isa = PBXResourcesBuildPhase;
258 | buildActionMask = 2147483647;
259 | files = (
260 | 9C7992AB25701B1D0072BEE2 /* Preview Assets.xcassets in Resources */,
261 | 9C7992A825701B1D0072BEE2 /* Assets.xcassets in Resources */,
262 | );
263 | runOnlyForDeploymentPostprocessing = 0;
264 | };
265 | 9C7992B425701B1D0072BEE2 /* Resources */ = {
266 | isa = PBXResourcesBuildPhase;
267 | buildActionMask = 2147483647;
268 | files = (
269 | );
270 | runOnlyForDeploymentPostprocessing = 0;
271 | };
272 | 9C7992BF25701B1D0072BEE2 /* Resources */ = {
273 | isa = PBXResourcesBuildPhase;
274 | buildActionMask = 2147483647;
275 | files = (
276 | );
277 | runOnlyForDeploymentPostprocessing = 0;
278 | };
279 | /* End PBXResourcesBuildPhase section */
280 |
281 | /* Begin PBXSourcesBuildPhase section */
282 | 9C79929C25701B180072BEE2 /* Sources */ = {
283 | isa = PBXSourcesBuildPhase;
284 | buildActionMask = 2147483647;
285 | files = (
286 | 9C7992E625701D170072BEE2 /* Forecast.swift in Sources */,
287 | 9C7992D725701C770072BEE2 /* WeatherAPIClient.swift in Sources */,
288 | 9C7992AD25701B1D0072BEE2 /* Persistence.swift in Sources */,
289 | 9C7992DC25701CC10072BEE2 /* SunDial.swift in Sources */,
290 | 9C7992A625701B180072BEE2 /* ContentView.swift in Sources */,
291 | 9C7992E125701CDB0072BEE2 /* Settings.swift in Sources */,
292 | 9C7992B025701B1D0072BEE2 /* sunshine.xcdatamodeld in Sources */,
293 | 9C7992F025701D8A0072BEE2 /* SearchBar.swift in Sources */,
294 | 9C7992EB25701D530072BEE2 /* LocationService.swift in Sources */,
295 | 9C7992A425701B180072BEE2 /* sunshineApp.swift in Sources */,
296 | );
297 | runOnlyForDeploymentPostprocessing = 0;
298 | };
299 | 9C7992B225701B1D0072BEE2 /* Sources */ = {
300 | isa = PBXSourcesBuildPhase;
301 | buildActionMask = 2147483647;
302 | files = (
303 | 9C7992BB25701B1D0072BEE2 /* sunshineTests.swift in Sources */,
304 | );
305 | runOnlyForDeploymentPostprocessing = 0;
306 | };
307 | 9C7992BD25701B1D0072BEE2 /* Sources */ = {
308 | isa = PBXSourcesBuildPhase;
309 | buildActionMask = 2147483647;
310 | files = (
311 | 9C7992C625701B1D0072BEE2 /* sunshineUITests.swift in Sources */,
312 | );
313 | runOnlyForDeploymentPostprocessing = 0;
314 | };
315 | /* End PBXSourcesBuildPhase section */
316 |
317 | /* Begin PBXTargetDependency section */
318 | 9C7992B825701B1D0072BEE2 /* PBXTargetDependency */ = {
319 | isa = PBXTargetDependency;
320 | target = 9C79929F25701B180072BEE2 /* sunshine */;
321 | targetProxy = 9C7992B725701B1D0072BEE2 /* PBXContainerItemProxy */;
322 | };
323 | 9C7992C325701B1D0072BEE2 /* PBXTargetDependency */ = {
324 | isa = PBXTargetDependency;
325 | target = 9C79929F25701B180072BEE2 /* sunshine */;
326 | targetProxy = 9C7992C225701B1D0072BEE2 /* PBXContainerItemProxy */;
327 | };
328 | /* End PBXTargetDependency section */
329 |
330 | /* Begin XCBuildConfiguration section */
331 | 9C7992C825701B1D0072BEE2 /* Debug */ = {
332 | isa = XCBuildConfiguration;
333 | buildSettings = {
334 | ALWAYS_SEARCH_USER_PATHS = NO;
335 | CLANG_ANALYZER_NONNULL = YES;
336 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
338 | CLANG_CXX_LIBRARY = "libc++";
339 | CLANG_ENABLE_MODULES = YES;
340 | CLANG_ENABLE_OBJC_ARC = YES;
341 | CLANG_ENABLE_OBJC_WEAK = YES;
342 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
343 | CLANG_WARN_BOOL_CONVERSION = YES;
344 | CLANG_WARN_COMMA = YES;
345 | CLANG_WARN_CONSTANT_CONVERSION = YES;
346 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
347 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
348 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
349 | CLANG_WARN_EMPTY_BODY = YES;
350 | CLANG_WARN_ENUM_CONVERSION = YES;
351 | CLANG_WARN_INFINITE_RECURSION = YES;
352 | CLANG_WARN_INT_CONVERSION = YES;
353 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
354 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
355 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
356 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
357 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
358 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
359 | CLANG_WARN_STRICT_PROTOTYPES = YES;
360 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
361 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
362 | CLANG_WARN_UNREACHABLE_CODE = YES;
363 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
364 | COPY_PHASE_STRIP = NO;
365 | DEBUG_INFORMATION_FORMAT = dwarf;
366 | ENABLE_STRICT_OBJC_MSGSEND = YES;
367 | ENABLE_TESTABILITY = YES;
368 | GCC_C_LANGUAGE_STANDARD = gnu11;
369 | GCC_DYNAMIC_NO_PIC = NO;
370 | GCC_NO_COMMON_BLOCKS = YES;
371 | GCC_OPTIMIZATION_LEVEL = 0;
372 | GCC_PREPROCESSOR_DEFINITIONS = (
373 | "DEBUG=1",
374 | "$(inherited)",
375 | );
376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
378 | GCC_WARN_UNDECLARED_SELECTOR = YES;
379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
380 | GCC_WARN_UNUSED_FUNCTION = YES;
381 | GCC_WARN_UNUSED_VARIABLE = YES;
382 | IPHONEOS_DEPLOYMENT_TARGET = 14.2;
383 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
384 | MTL_FAST_MATH = YES;
385 | ONLY_ACTIVE_ARCH = YES;
386 | SDKROOT = iphoneos;
387 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
388 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
389 | };
390 | name = Debug;
391 | };
392 | 9C7992C925701B1D0072BEE2 /* Release */ = {
393 | isa = XCBuildConfiguration;
394 | buildSettings = {
395 | ALWAYS_SEARCH_USER_PATHS = NO;
396 | CLANG_ANALYZER_NONNULL = YES;
397 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
398 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
399 | CLANG_CXX_LIBRARY = "libc++";
400 | CLANG_ENABLE_MODULES = YES;
401 | CLANG_ENABLE_OBJC_ARC = YES;
402 | CLANG_ENABLE_OBJC_WEAK = YES;
403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
404 | CLANG_WARN_BOOL_CONVERSION = YES;
405 | CLANG_WARN_COMMA = YES;
406 | CLANG_WARN_CONSTANT_CONVERSION = YES;
407 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
408 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
409 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
410 | CLANG_WARN_EMPTY_BODY = YES;
411 | CLANG_WARN_ENUM_CONVERSION = YES;
412 | CLANG_WARN_INFINITE_RECURSION = YES;
413 | CLANG_WARN_INT_CONVERSION = YES;
414 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
415 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
416 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
417 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
418 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
419 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
420 | CLANG_WARN_STRICT_PROTOTYPES = YES;
421 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
422 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
423 | CLANG_WARN_UNREACHABLE_CODE = YES;
424 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
425 | COPY_PHASE_STRIP = NO;
426 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
427 | ENABLE_NS_ASSERTIONS = NO;
428 | ENABLE_STRICT_OBJC_MSGSEND = YES;
429 | GCC_C_LANGUAGE_STANDARD = gnu11;
430 | GCC_NO_COMMON_BLOCKS = YES;
431 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
432 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
433 | GCC_WARN_UNDECLARED_SELECTOR = YES;
434 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
435 | GCC_WARN_UNUSED_FUNCTION = YES;
436 | GCC_WARN_UNUSED_VARIABLE = YES;
437 | IPHONEOS_DEPLOYMENT_TARGET = 14.2;
438 | MTL_ENABLE_DEBUG_INFO = NO;
439 | MTL_FAST_MATH = YES;
440 | SDKROOT = iphoneos;
441 | SWIFT_COMPILATION_MODE = wholemodule;
442 | SWIFT_OPTIMIZATION_LEVEL = "-O";
443 | VALIDATE_PRODUCT = YES;
444 | };
445 | name = Release;
446 | };
447 | 9C7992CB25701B1D0072BEE2 /* Debug */ = {
448 | isa = XCBuildConfiguration;
449 | buildSettings = {
450 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
451 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
452 | CODE_SIGN_STYLE = Automatic;
453 | DEVELOPMENT_ASSET_PATHS = "\"sunshine/Preview Content\"";
454 | DEVELOPMENT_TEAM = SF87CBF59Z;
455 | ENABLE_PREVIEWS = YES;
456 | INFOPLIST_FILE = sunshine/Info.plist;
457 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
458 | LD_RUNPATH_SEARCH_PATHS = (
459 | "$(inherited)",
460 | "@executable_path/Frameworks",
461 | );
462 | PRODUCT_BUNDLE_IDENTIFIER = com.maximeheckel.sunshine;
463 | PRODUCT_NAME = "$(TARGET_NAME)";
464 | SWIFT_VERSION = 5.0;
465 | TARGETED_DEVICE_FAMILY = 1;
466 | };
467 | name = Debug;
468 | };
469 | 9C7992CC25701B1D0072BEE2 /* Release */ = {
470 | isa = XCBuildConfiguration;
471 | buildSettings = {
472 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
473 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
474 | CODE_SIGN_STYLE = Automatic;
475 | DEVELOPMENT_ASSET_PATHS = "\"sunshine/Preview Content\"";
476 | DEVELOPMENT_TEAM = SF87CBF59Z;
477 | ENABLE_PREVIEWS = YES;
478 | INFOPLIST_FILE = sunshine/Info.plist;
479 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
480 | LD_RUNPATH_SEARCH_PATHS = (
481 | "$(inherited)",
482 | "@executable_path/Frameworks",
483 | );
484 | PRODUCT_BUNDLE_IDENTIFIER = com.maximeheckel.sunshine;
485 | PRODUCT_NAME = "$(TARGET_NAME)";
486 | SWIFT_VERSION = 5.0;
487 | TARGETED_DEVICE_FAMILY = 1;
488 | };
489 | name = Release;
490 | };
491 | 9C7992CE25701B1D0072BEE2 /* Debug */ = {
492 | isa = XCBuildConfiguration;
493 | buildSettings = {
494 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
495 | BUNDLE_LOADER = "$(TEST_HOST)";
496 | CODE_SIGN_STYLE = Automatic;
497 | DEVELOPMENT_TEAM = SF87CBF59Z;
498 | INFOPLIST_FILE = sunshineTests/Info.plist;
499 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
500 | LD_RUNPATH_SEARCH_PATHS = (
501 | "$(inherited)",
502 | "@executable_path/Frameworks",
503 | "@loader_path/Frameworks",
504 | );
505 | PRODUCT_BUNDLE_IDENTIFIER = com.maximeheckel.sunshineTests;
506 | PRODUCT_NAME = "$(TARGET_NAME)";
507 | SWIFT_VERSION = 5.0;
508 | TARGETED_DEVICE_FAMILY = "1,2";
509 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sunshine.app/sunshine";
510 | };
511 | name = Debug;
512 | };
513 | 9C7992CF25701B1D0072BEE2 /* Release */ = {
514 | isa = XCBuildConfiguration;
515 | buildSettings = {
516 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
517 | BUNDLE_LOADER = "$(TEST_HOST)";
518 | CODE_SIGN_STYLE = Automatic;
519 | DEVELOPMENT_TEAM = SF87CBF59Z;
520 | INFOPLIST_FILE = sunshineTests/Info.plist;
521 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
522 | LD_RUNPATH_SEARCH_PATHS = (
523 | "$(inherited)",
524 | "@executable_path/Frameworks",
525 | "@loader_path/Frameworks",
526 | );
527 | PRODUCT_BUNDLE_IDENTIFIER = com.maximeheckel.sunshineTests;
528 | PRODUCT_NAME = "$(TARGET_NAME)";
529 | SWIFT_VERSION = 5.0;
530 | TARGETED_DEVICE_FAMILY = "1,2";
531 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sunshine.app/sunshine";
532 | };
533 | name = Release;
534 | };
535 | 9C7992D125701B1D0072BEE2 /* Debug */ = {
536 | isa = XCBuildConfiguration;
537 | buildSettings = {
538 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
539 | CODE_SIGN_STYLE = Automatic;
540 | DEVELOPMENT_TEAM = SF87CBF59Z;
541 | INFOPLIST_FILE = sunshineUITests/Info.plist;
542 | LD_RUNPATH_SEARCH_PATHS = (
543 | "$(inherited)",
544 | "@executable_path/Frameworks",
545 | "@loader_path/Frameworks",
546 | );
547 | PRODUCT_BUNDLE_IDENTIFIER = com.maximeheckel.sunshineUITests;
548 | PRODUCT_NAME = "$(TARGET_NAME)";
549 | SWIFT_VERSION = 5.0;
550 | TARGETED_DEVICE_FAMILY = "1,2";
551 | TEST_TARGET_NAME = sunshine;
552 | };
553 | name = Debug;
554 | };
555 | 9C7992D225701B1D0072BEE2 /* Release */ = {
556 | isa = XCBuildConfiguration;
557 | buildSettings = {
558 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
559 | CODE_SIGN_STYLE = Automatic;
560 | DEVELOPMENT_TEAM = SF87CBF59Z;
561 | INFOPLIST_FILE = sunshineUITests/Info.plist;
562 | LD_RUNPATH_SEARCH_PATHS = (
563 | "$(inherited)",
564 | "@executable_path/Frameworks",
565 | "@loader_path/Frameworks",
566 | );
567 | PRODUCT_BUNDLE_IDENTIFIER = com.maximeheckel.sunshineUITests;
568 | PRODUCT_NAME = "$(TARGET_NAME)";
569 | SWIFT_VERSION = 5.0;
570 | TARGETED_DEVICE_FAMILY = "1,2";
571 | TEST_TARGET_NAME = sunshine;
572 | };
573 | name = Release;
574 | };
575 | /* End XCBuildConfiguration section */
576 |
577 | /* Begin XCConfigurationList section */
578 | 9C79929B25701B180072BEE2 /* Build configuration list for PBXProject "sunshine" */ = {
579 | isa = XCConfigurationList;
580 | buildConfigurations = (
581 | 9C7992C825701B1D0072BEE2 /* Debug */,
582 | 9C7992C925701B1D0072BEE2 /* Release */,
583 | );
584 | defaultConfigurationIsVisible = 0;
585 | defaultConfigurationName = Release;
586 | };
587 | 9C7992CA25701B1D0072BEE2 /* Build configuration list for PBXNativeTarget "sunshine" */ = {
588 | isa = XCConfigurationList;
589 | buildConfigurations = (
590 | 9C7992CB25701B1D0072BEE2 /* Debug */,
591 | 9C7992CC25701B1D0072BEE2 /* Release */,
592 | );
593 | defaultConfigurationIsVisible = 0;
594 | defaultConfigurationName = Release;
595 | };
596 | 9C7992CD25701B1D0072BEE2 /* Build configuration list for PBXNativeTarget "sunshineTests" */ = {
597 | isa = XCConfigurationList;
598 | buildConfigurations = (
599 | 9C7992CE25701B1D0072BEE2 /* Debug */,
600 | 9C7992CF25701B1D0072BEE2 /* Release */,
601 | );
602 | defaultConfigurationIsVisible = 0;
603 | defaultConfigurationName = Release;
604 | };
605 | 9C7992D025701B1D0072BEE2 /* Build configuration list for PBXNativeTarget "sunshineUITests" */ = {
606 | isa = XCConfigurationList;
607 | buildConfigurations = (
608 | 9C7992D125701B1D0072BEE2 /* Debug */,
609 | 9C7992D225701B1D0072BEE2 /* Release */,
610 | );
611 | defaultConfigurationIsVisible = 0;
612 | defaultConfigurationName = Release;
613 | };
614 | /* End XCConfigurationList section */
615 |
616 | /* Begin XCVersionGroup section */
617 | 9C7992AE25701B1D0072BEE2 /* sunshine.xcdatamodeld */ = {
618 | isa = XCVersionGroup;
619 | children = (
620 | 9C7992AF25701B1D0072BEE2 /* sunshine.xcdatamodel */,
621 | );
622 | currentVersion = 9C7992AF25701B1D0072BEE2 /* sunshine.xcdatamodel */;
623 | path = sunshine.xcdatamodeld;
624 | sourceTree = "";
625 | versionGroupType = wrapper.xcdatamodel;
626 | };
627 | /* End XCVersionGroup section */
628 | };
629 | rootObject = 9C79929825701B180072BEE2 /* Project object */;
630 | }
631 |
--------------------------------------------------------------------------------
/sunshine/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // sunshine
4 | //
5 | // Created by Maxime on 8/22/20.
6 | //
7 |
8 | import SwiftUI
9 | import CoreData
10 |
11 | let gradients: [MainDaypart: [Color]]! = [
12 | MainDaypart.dawn: [Color("Pink Dawn"), Color("Blue Dawn End Gradient")],
13 | MainDaypart.mid: [Color("Blue Mid Day"), Color("Blue Mid Day End Gradient")],
14 | MainDaypart.dusk: [Color("Blue Dusk"), Color("Peach Dusk End Gradient")],
15 | MainDaypart.night: [Color("Blue Night"), Color("Blue Night End Gradient")]]
16 |
17 | func getDayPart(sunrise: Double, sunset: Double, nextDaySunrise: Double, timestamp: Double) -> MainDaypart {
18 | let dawnStart = sunrise - 86400 * 0.03
19 | let dawnEnd = sunrise + 86400 * 0.05
20 | let duskStart = sunset - 86400 * 0.03
21 | let duskEnd = sunset + 86400 * 0.05
22 |
23 | let nextDawnStart = nextDaySunrise - 86400 * 0.03
24 | let nextDawnEnd = nextDaySunrise + 86400 * 0.05
25 |
26 | if (timestamp > sunset && timestamp > dawnStart && timestamp < nextDawnStart) {
27 | // Current day after sunset before next sunset (sunrise is day 1 sunrise thus < timestramp)
28 | return MainDaypart.night
29 | }
30 |
31 | if (timestamp < duskEnd && timestamp < dawnStart) {
32 | // Next day, timestamp is both < sunrise and < sunset
33 | return MainDaypart.night
34 | }
35 |
36 | if (timestamp > dawnStart && timestamp < dawnEnd) {
37 | // Current day sunrise
38 | return MainDaypart.dawn
39 | }
40 |
41 | if (timestamp > nextDawnStart && timestamp < nextDawnEnd) {
42 | // Next day sunrise (for hourly forecast)
43 | return MainDaypart.dawn
44 | }
45 |
46 | if (timestamp > duskStart && timestamp < duskEnd) {
47 | return MainDaypart.dusk
48 | }
49 |
50 | return MainDaypart.mid
51 | }
52 |
53 | func kelvinToFarenheit(_ kelvin: Double) -> Double {
54 | return kelvin * 1.8 - 459.67
55 | }
56 |
57 | func kelvinToCelsius(_ celsius: Double) -> Double {
58 | return celsius - 273.15
59 | }
60 |
61 | func metersPerSecToMPH(_ mps: Double) -> Double {
62 | return mps / 0.44704
63 | }
64 |
65 | func metersPerSecToKMH(_ mps: Double) -> Double {
66 | return mps * 18 / 5
67 | }
68 |
69 | struct FormattedTemperature: View {
70 | var temperature: Double
71 | var unit: Temperature
72 |
73 | @ViewBuilder var body: some View {
74 | switch(unit) {
75 | case Temperature.fahrenheit:
76 | Text(String(format: "%.0f°F", kelvinToFarenheit(temperature)))
77 | case Temperature.celsius:
78 | Text(String(format: "%.0f°C", kelvinToCelsius(temperature)))
79 | default:
80 | Text(String(format: "%.0f°K", temperature))
81 | }
82 |
83 | }
84 | }
85 |
86 | struct FormattedSpeed: View {
87 | var speed: Double
88 | var unit: Speed
89 |
90 | @ViewBuilder var body: some View {
91 | switch(unit) {
92 | case Speed.mph:
93 | Text(String(format: "%.1f mph", metersPerSecToMPH(speed)))
94 | case Speed.kmh:
95 | Text(String(format: "%.1f km/h", metersPerSecToKMH(speed)))
96 | default:
97 | Text(String(format: "%.1f m/s", speed))
98 |
99 | }
100 | }
101 | }
102 |
103 | let REFRESH_INTERVAL = 1800
104 |
105 |
106 |
107 | struct VisualEffectView: UIViewRepresentable {
108 | var effect: UIVisualEffect?
109 | func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() }
110 | func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = effect }
111 | }
112 |
113 | struct BottomSheetView: View {
114 | @Binding var isOpen: Bool
115 | @GestureState private var translation: CGFloat = 0
116 |
117 | let maxHeight: CGFloat
118 | let minHeight: CGFloat
119 | let content: Content
120 |
121 | init(isOpen: Binding, maxHeight: CGFloat, @ViewBuilder content: () -> Content) {
122 | self.minHeight = maxHeight * 1/7
123 | self.maxHeight = maxHeight
124 | self.content = content()
125 | self._isOpen = isOpen
126 | }
127 |
128 | private var offset: CGFloat {
129 | isOpen ? 0 : maxHeight - minHeight
130 | }
131 |
132 | private var indicator: some View {
133 | RoundedRectangle(cornerRadius: 50)
134 | .fill(Color.secondary)
135 | .frame(
136 | width: 100,
137 | height: 5
138 | )
139 | }
140 |
141 | var body: some View {
142 | GeometryReader { geometry in
143 | VStack(spacing: 0) {
144 | self.indicator.padding()
145 | self.content
146 | }
147 | .frame(width: geometry.size.width, height: self.maxHeight, alignment: .top)
148 | .background(Color(.tertiarySystemBackground))
149 | .cornerRadius(25)
150 | .frame(width: geometry.size.width, height: geometry.size.height, alignment: .bottom)
151 | .offset(y: max(self.offset + self.translation, 0))
152 | .animation(.interactiveSpring())
153 | .gesture(
154 | DragGesture().updating(self.$translation) { value, state, _ in
155 | state = value.translation.height
156 | }.onEnded { value in
157 | let snapDistance = self.maxHeight * 0.2
158 | guard abs(value.translation.height) > snapDistance else {
159 | return
160 | }
161 | self.isOpen = value.translation.height < 0
162 | }
163 | )
164 | }
165 | }
166 | }
167 |
168 | struct MetricsRow: View {
169 | var forecast: Response
170 | var speedUnit: Speed
171 |
172 | @State private var toggleAnimation = false
173 |
174 | var body: some View {
175 | VStack {
176 | HStack(spacing: 20) {
177 | HStack {
178 | Image("humidity")
179 | .resizable()
180 | .scaledToFit()
181 | .frame(width: 14)
182 | Text("\(forecast.current.humidity)%")
183 | .font(.system(size: 12, weight: .medium))
184 | .foregroundColor(.black)
185 | }
186 | HStack {
187 | Image(systemName: "wind")
188 | .foregroundColor(.black)
189 | FormattedSpeed(speed: forecast.current.windSpeed, unit: speedUnit)
190 | .font(.system(size: 12, weight: .medium))
191 | .foregroundColor(.black)
192 | }
193 | HStack {
194 | Image(systemName: "location")
195 | .foregroundColor(.black)
196 | .rotationEffect(.degrees(Double(45 - forecast.current.windDeg)))
197 | .animation(.spring())
198 | Text("\(forecast.current.windDeg)°")
199 | .font(.system(size: 12, weight: .medium))
200 | .foregroundColor(.black)
201 | }
202 | HStack {
203 | Image(systemName: "cloud")
204 | .foregroundColor(.black)
205 | Text("\(forecast.current.clouds)%")
206 | .font(.system(size: 12, weight: .medium))
207 | .foregroundColor(.black)
208 | }
209 | }
210 | .padding(.top, 10)
211 | }
212 | .frame(height: 35)
213 | .onAppear {
214 | self.toggleAnimation = true
215 | }
216 | }
217 | }
218 |
219 | struct MainAssetByDaypart: View {
220 | @State private var toggleAnimation = false
221 | var currentDayPart: MainDaypart
222 |
223 | @ViewBuilder var body: some View {
224 | if (currentDayPart == MainDaypart.night) {
225 | Image("moon")
226 | .resizable()
227 | .frame(width: 140.0, height: 140.0)
228 | .opacity(toggleAnimation ? 1: 0)
229 | .animation(Animation.easeInOut(duration: 2.0).delay(0.7))
230 | .onAppear {
231 | self.toggleAnimation = true
232 | }
233 | }
234 |
235 | if (currentDayPart == MainDaypart.mid) {
236 | Image("sun-mid").resizable()
237 | .frame(width: 200.0, height: 200.0)
238 | .offset(y: toggleAnimation ? 0 : 100)
239 | .animation(Animation.easeInOut(duration: 2.0).delay(0.7))
240 | .onAppear {
241 | self.toggleAnimation = true
242 | }
243 | }
244 |
245 | if(currentDayPart == MainDaypart.dusk) {
246 | Image("sun-dawn").resizable()
247 | .frame(width: 200.0, height: 200.0)
248 | .opacity(toggleAnimation ? 1 : 0)
249 | .offset(y: toggleAnimation ? 100 : 0)
250 | .animation(Animation.easeInOut(duration: 2.0).delay(0.5).delay(0.7))
251 | .onAppear {
252 | self.toggleAnimation = true
253 | }
254 | }
255 |
256 | if(currentDayPart == MainDaypart.dawn) {
257 | Image("sun-dawn").resizable()
258 | .frame(width: 200.0, height: 200.0)
259 | .offset(y: toggleAnimation ? 100 : 180)
260 | .animation(Animation.easeInOut(duration: 2.0).delay(0.7))
261 | .onAppear {
262 | self.toggleAnimation = true
263 | }
264 | }
265 | }
266 | }
267 |
268 | func isCloudy(_ weatherId: Int) -> Bool {
269 | if (weatherId > 802 || weatherId >= 500 && weatherId < 600 || weatherId >= 200 && weatherId < 300 || weatherId >= 600 && weatherId < 700) {
270 | return true
271 | }
272 |
273 | return false
274 | }
275 |
276 | struct MainWeatherImage: View {
277 | @State private var toggleAnimation = false
278 | var weather: WeatherObject
279 | var currentDayPart: MainDaypart
280 |
281 | var isMainAssetHigh: Bool {
282 | return currentDayPart == MainDaypart.mid || currentDayPart == MainDaypart.night
283 | }
284 |
285 | @ViewBuilder var body: some View {
286 | ZStack {
287 | if (weather.id >= 800) {
288 | MainAssetByDaypart(currentDayPart: currentDayPart)
289 |
290 | if (weather.id == 801 || weather.id == 802) {
291 | Image("cloud")
292 | .resizable()
293 | .scaledToFit()
294 | .frame(width: 130, height: 100)
295 | .offset(x: toggleAnimation ? 40 : 300, y: isMainAssetHigh ? 20 : 100)
296 | .animation(Animation.easeInOut(duration: toggleAnimation ? 3.0 : 0).delay(1))
297 | Image("cloud")
298 | .resizable()
299 | .scaledToFit()
300 | .frame(width: 130, height: 90)
301 | .offset(x: toggleAnimation ? -20 : -300, y: isMainAssetHigh ? -10 : 70)
302 | .animation(Animation.easeInOut(duration: toggleAnimation ? 3.0 : 0).delay(0.7))
303 | }
304 |
305 | if (weather.id > 802) {
306 | // Broken clouds / overcast
307 | Image("big-cloud")
308 | .resizable()
309 | .scaledToFit()
310 | .frame(width: 130, height: 100)
311 | .offset(x: toggleAnimation ? 0 : 300, y: isMainAssetHigh ? 20: 50)
312 | .animation(Animation.easeInOut(duration: toggleAnimation ? 3.0 : 0))
313 | }
314 | }
315 |
316 | if (weather.id >= 500 && weather.id < 600) {
317 | if (weather.id <= 501) {
318 | Image("light-rain-drops")
319 | .resizable()
320 | .scaledToFit()
321 | .frame(width: 100, height: 50)
322 |
323 | } else {
324 | Image("heavy-rain-drops")
325 | .resizable()
326 | .scaledToFit()
327 | .frame(width: 100, height: 50)
328 |
329 | }
330 | }
331 |
332 | if (weather.id >= 300 && weather.id < 400) {
333 | if (weather.id <= 311) {
334 | Image("light-rain-drops")
335 | .resizable()
336 |
337 | } else {
338 | Image("heavy-rain-drops")
339 | .resizable()
340 |
341 | }
342 | }
343 |
344 | if (weather.id >= 200 && weather.id < 300) {
345 | // thunderstorm
346 | MainAssetByDaypart(currentDayPart: currentDayPart)
347 | }
348 |
349 | if (weather.id >= 600 && weather.id < 700) {
350 | // snow
351 | MainAssetByDaypart(currentDayPart: currentDayPart)
352 | }
353 |
354 | if (weather.id >= 700 && weather.id < 800) {
355 | // atmospheric
356 | MainAssetByDaypart(currentDayPart: currentDayPart)
357 | }
358 |
359 | else {
360 | // MainAssetByDaypart(currentDayPart: currentDayPart)
361 | }
362 | }
363 | .onAppear {
364 | self.toggleAnimation = true
365 | }
366 | }
367 | }
368 |
369 | struct MainCard: View {
370 |
371 | var forecast: Response
372 | var currentDayPart: MainDaypart
373 | var tempUnit: Temperature
374 | var speedUnit: Speed
375 |
376 | var gradient: [Color] {
377 | return gradients[currentDayPart] ?? [Color.white]
378 | }
379 |
380 | var body: some View {
381 | VStack {
382 | ZStack {
383 | // Main Card with background and little drop shaddow based on time of day (daypart)
384 | RoundedRectangle(cornerRadius: 25)
385 | .fill(LinearGradient(
386 | gradient: Gradient(colors: gradient),
387 | startPoint: UnitPoint(x: 0.5, y: 0),
388 | endPoint: UnitPoint(x: 0.5, y: 0.6)))
389 | .shadow(color: Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.25)), radius:10, x:0, y:4)
390 |
391 | // Cloudy overlay
392 | if (isCloudy(forecast.current.weather[0].id)) {
393 | if (currentDayPart == MainDaypart.night) {
394 | RoundedRectangle(cornerRadius: 25)
395 | .fill(LinearGradient(
396 | gradient: Gradient(stops: [
397 | .init(color: Color(#colorLiteral(red: 0.09019608050584793, green: 0.1411764770746231, blue: 0.16862745583057404, alpha: 1)), location: 0),
398 | .init(color: Color(#colorLiteral(red: 0.250980406999588, green: 0.2705882489681244, blue: 0.3294117748737335, alpha: 1)), location: 1)]),
399 | startPoint: UnitPoint(x: 0.5, y: 0),
400 | endPoint: UnitPoint(x: 0.5, y: 0.5)))
401 | .opacity(0.7)
402 | } else {
403 | RoundedRectangle(cornerRadius: 25)
404 | .fill(LinearGradient(
405 | gradient: Gradient(stops: [
406 | .init(color: Color(#colorLiteral(red: 0.6976562142, green: 0.7035937309, blue: 0.7124999762, alpha: 0.700952492)), location: 0),
407 | .init(color: Color(#colorLiteral(red: 0.8545138835906982, green: 0.8718518614768982, blue: 0.8916666507720947, alpha: 0.20000000298023224)), location: 1)]),
408 | startPoint: UnitPoint(x: 0.5, y: 0),
409 | endPoint: UnitPoint(x: 0.5, y: 0.5)))
410 | .opacity(0.7)
411 | }
412 | }
413 |
414 | // Assets based on weather and time (each asset may have it's own position)
415 | GeometryReader { geometry in
416 | MainWeatherImage(weather: forecast.current.weather[0], currentDayPart: currentDayPart)
417 | .frame(width: geometry.size.width, height: 260)
418 | .clipped()
419 | }
420 |
421 | // Temperature, conditions, feels like
422 | VStack {
423 | HStack {
424 | FormattedTemperature(temperature: forecast.current.temp, unit: tempUnit).font(.system(size: 36, weight: .medium)).foregroundColor(.white)
425 | Spacer()
426 | VStack(alignment: .trailing) {
427 | Text("\(forecast.current.weather[0].description.capitalized)").font(.system(size: 24, weight: .medium)).foregroundColor(.white)
428 | HStack(spacing: 3) {
429 | Text("Feels like")
430 | .font(.system(size: 14, weight: .semibold)).foregroundColor(.white)
431 | FormattedTemperature(temperature: forecast.current.feelsLike, unit: tempUnit)
432 | .font(.system(size: 14, weight: .semibold)).foregroundColor(.white)
433 | }
434 | }
435 |
436 | }.padding(15)
437 |
438 | Spacer()
439 |
440 | ZStack {
441 | VisualEffectView(effect: UIBlurEffect(style: .light))
442 | .cornerRadius(25)
443 | VStack {
444 | MetricsRow(forecast: forecast, speedUnit: speedUnit)
445 | Rectangle()
446 | .fill(Color.black)
447 | .frame(height: 1)
448 | .opacity(0.05)
449 | if (currentDayPart != MainDaypart.night) {
450 | SunDial(sunrise: forecast.current.sunrise, sunset: forecast.current.sunset, timestamp: forecast.current.dt, timezone: forecast.timezone)
451 | } else {
452 | VStack {
453 | Text("Next sunrise at \(getNextSunriseTime(forecast.current.sunrise))")
454 | .font(.system(size: 14, weight: .medium))
455 | .foregroundColor(.black)
456 |
457 | }
458 | .frame(height: 70)
459 | .padding(.leading,5)
460 | .padding(.trailing,5)
461 | }
462 | }
463 |
464 | }
465 | .frame(height:currentDayPart == MainDaypart.night ? 125 : 230)
466 | }
467 | }
468 | }
469 | .frame(height: currentDayPart == MainDaypart.night ? 385 : 475)
470 | .padding(.leading,8)
471 | .padding(.trailing,8)
472 | }
473 |
474 |
475 | func getNextSunriseTime(_ sunrise: Int) -> String {
476 | let date = Date(timeIntervalSince1970: Double(sunrise))
477 | let formatter = DateFormatter()
478 | formatter.timeStyle = .short
479 | formatter.locale = NSLocale.current
480 | formatter.timeZone = TimeZone(identifier: forecast.timezone)
481 | var formattedDate:String {formatter.string(from: date)}
482 |
483 | return formattedDate
484 | }
485 | }
486 |
487 |
488 |
489 | struct MainWeatherScreen: View {
490 | var forecast: Response!
491 | var isSelected: Bool
492 | var name: String
493 | var status: ViewState
494 | var tempUnit: Temperature
495 | var speedUnit: Speed
496 |
497 | var currentDayPart: MainDaypart {
498 | return getDayPart(sunrise: Double(forecast.current.sunrise), sunset: Double(forecast.current.sunset), nextDaySunrise: Double(forecast.daily[1].sunrise), timestamp: Double(forecast.current.dt))
499 | }
500 |
501 |
502 | var body: some View {
503 | VStack(spacing: 0) {
504 | if (status == ViewState.loading && forecast == nil) {
505 | VStack(alignment: .leading) {
506 | Text(" ").font(.system(size: 16, weight: .medium, design: Font.Design.default)).foregroundColor(Color(#colorLiteral(red: 0.5920000076293945, green: 0.5920000076293945, blue: 0.5920000076293945, alpha: 1)))
507 | HStack {
508 | Text("\(name)").font(.system(size: 24, weight: .medium))
509 | Spacer()
510 | }
511 | }
512 | .padding([.leading, .trailing], 20)
513 | .frame(height: 105, alignment: .leading)
514 | Spacer()
515 | }
516 | if (status == ViewState.error) {
517 | Text("Error :(")
518 | }
519 | if (status != ViewState.error && forecast != nil) {
520 | VStack(alignment: .leading) {
521 | Text("\(getDate(forecast.current.dt))").font(.system(size: 16, weight: .bold, design: Font.Design.default)).foregroundColor(Color(#colorLiteral(red: 0.5920000076293945, green: 0.5920000076293945, blue: 0.5920000076293945, alpha: 1)))
522 | HStack {
523 | Text("\(name)").font(.system(size: 24, weight: .medium))
524 | Spacer()
525 | }
526 |
527 | }
528 | .padding([.leading, .trailing], 20)
529 | .frame(height: 105, alignment: .leading)
530 |
531 | // Main Card
532 | VStack {
533 | if (isSelected) {
534 | MainCard(forecast: forecast, currentDayPart: currentDayPart, tempUnit: tempUnit, speedUnit: speedUnit)
535 | if(currentDayPart == MainDaypart.night){
536 | if (forecast.current.dt > forecast.current.sunset) {
537 | DailyCard(forecast: forecast.daily[1], tempUnit: tempUnit)
538 | .padding([.leading, .trailing], 10)
539 | .padding([.top], 10)
540 | } else {
541 | DailyCard(forecast: forecast.daily[0], tempUnit: tempUnit)
542 | .padding([.leading, .trailing], 10)
543 | .padding([.top], 10)
544 | }
545 | }
546 | Spacer()
547 | } else {
548 | Spacer()
549 | }
550 | }
551 | .transition(.opacity)
552 | .animation(.easeInOut)
553 | }
554 |
555 | }
556 | .transition(.opacity)
557 | .animation(.easeInOut)
558 | }
559 |
560 | func getDate(_ timestamp: Int) -> String {
561 | var date: Date { Date(timeIntervalSince1970: Double(timestamp)) }
562 | let formatter = DateFormatter()
563 | formatter.dateFormat = "E MMM dd"
564 | formatter.timeZone = TimeZone(identifier: forecast.timezone)
565 | var formattedDate:String {formatter.string(from: date)}
566 |
567 | return formattedDate
568 | }
569 | }
570 |
571 | class ViewModel: ObservableObject {
572 | @Published var state: ViewState
573 | @Published var items: [Response] {
574 | didSet {
575 | self.focusedItem = items.indices.contains(selectTabIndex) ? items[selectTabIndex] : nil
576 | }
577 | }
578 | @Published var selectTabIndex: Int {
579 | didSet {
580 | self.focusedItem = items.indices.contains(selectTabIndex) ? items[selectTabIndex] : nil
581 | }
582 | }
583 | @Published var temperature: Temperature {
584 | didSet {
585 | UserDefaults.standard.set(temperature.rawValue, forKey: "temperature")
586 | }
587 | }
588 | @Published var speed: Speed {
589 | didSet {
590 | UserDefaults.standard.set(speed.rawValue, forKey: "speed")
591 | }
592 | }
593 |
594 | @Published var focusedItem: Response?
595 |
596 |
597 | let client = WeatherAPIClient()
598 |
599 | init() {
600 | self.focusedItem = nil
601 | self.selectTabIndex = 0
602 | self.state = ViewState.idle
603 | self.items = [Response]()
604 | self.temperature = (UserDefaults.standard.object(forKey: "temperature") == nil ? Temperature.fahrenheit : Temperature(rawValue: UserDefaults.standard.object(forKey: "temperature") as! Int)) ?? Temperature.fahrenheit
605 | self.speed = (UserDefaults.standard.object(forKey: "speed") == nil ? Speed.mph : Speed(rawValue: UserDefaults.standard.object(forKey: "speed") as! Int)) ?? Speed.mph
606 | }
607 |
608 |
609 |
610 | func refresh(_ index: Int, _ savedLocations: FetchedResults) {
611 | // print("Trying refresh at index \(index)")
612 | if (!items.indices.contains(index) && savedLocations.indices.contains(index)) {
613 | fetch(index, savedLocations)
614 | return
615 | } else if (savedLocations.indices.contains(index)) {
616 | let formatter = DateFormatter()
617 | formatter.dateFormat = "E, dd MMM, HH:mm:ss"
618 | var formattedDate:String {formatter.string(from: Date(timeIntervalSince1970: Double(items[index].current.dt)))}
619 |
620 | let diff = Int(Date().timeIntervalSince1970) - items[index].current.dt
621 |
622 | if (diff >= REFRESH_INTERVAL) {
623 | // print("Refresh")
624 | fetch(index, savedLocations)
625 | }
626 | }
627 | return
628 | }
629 |
630 | private func fetch(_ index: Int, _ savedLocations: FetchedResults) {
631 | if (savedLocations.indices.contains(index)) {
632 | self.state = ViewState.loading
633 | client.fetch(lat: savedLocations[index].lat, lon: savedLocations[index].lon) { result in
634 | DispatchQueue.main.async {
635 | switch result {
636 | case .success(let response):
637 | DispatchQueue.main.async {
638 | if (!self.items.indices.contains(index)) {
639 | self.items.append(response)
640 | } else {
641 | self.items[index] = response
642 | }
643 | self.state = ViewState.idle
644 |
645 | }
646 |
647 | case .failure(let error):
648 | print("Error! \(error)")
649 | self.state = ViewState.error
650 | }
651 |
652 | }
653 | }
654 | }
655 | }
656 | }
657 |
658 |
659 | struct ContentView: View {
660 | @Environment(\.managedObjectContext) private var viewContext
661 | @Environment(\.colorScheme) var colorScheme
662 | @StateObject var viewModel = ViewModel()
663 | @State private var loading = false
664 | @State private var error = false
665 | @State private var isSheetPresented = false
666 | @State private var bottomSheetShown = false
667 |
668 |
669 | @FetchRequest(entity: Location.entity(),sortDescriptors: [NSSortDescriptor(keyPath: \Location.timestamp, ascending: true)], animation: .default)
670 | private var savedLocations: FetchedResults
671 |
672 | let client = WeatherAPIClient()
673 |
674 | let locationSearchService = LocationSearchService()
675 | // let refreshTimer = Timer.publish(every: TimeInterval(3600), on: .main, in: .common).autoconnect()
676 |
677 |
678 | var body: some View {
679 | ZStack {
680 | Color(.secondarySystemBackground)
681 | .edgesIgnoringSafeArea(.all)
682 | VStack {
683 | // Top Bar
684 | HStack {
685 | Button(action: {
686 | self.isSheetPresented.toggle()
687 | }) {
688 | if(colorScheme == .dark) {
689 | Image("menu-light")
690 | } else {
691 | Image("menu-dark")
692 | }
693 | }
694 | Spacer()
695 | }
696 | .padding([.top, .leading], 20)
697 |
698 | // Center and Bottom View
699 | GeometryReader { geometry in
700 | if(savedLocations.count != 0 ){
701 | ZStack {
702 | TabView(selection: $viewModel.selectTabIndex){
703 | ForEach(savedLocations.indices) { index in
704 | MainWeatherScreen(forecast: viewModel.focusedItem, isSelected: viewModel.selectTabIndex == index, name: savedLocations[index].name!, status: viewModel.state, tempUnit: viewModel.temperature, speedUnit: viewModel.speed)
705 | .tag(index)
706 | }
707 | }
708 | .onReceive(viewModel.$selectTabIndex, perform: { index in
709 | viewModel.refresh(index, savedLocations)
710 | })
711 | .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
712 | .id(savedLocations.count)
713 |
714 | BottomSheetView(
715 | isOpen: self.$bottomSheetShown,
716 | maxHeight: geometry.size.height * 0.9
717 | ) {
718 | ForecastView()
719 | }
720 | .edgesIgnoringSafeArea(.bottom)
721 | }
722 | }
723 | }
724 | }
725 | .environmentObject(viewModel)
726 | }
727 | // .onReceive(refreshTimer){ _ in
728 | // self.refresh(store.selectTabIndex)
729 | // }
730 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
731 | if (viewModel.items.indices.contains(viewModel.selectTabIndex)) {
732 | viewModel.refresh(viewModel.selectTabIndex, savedLocations)
733 | }
734 | }
735 | .sheet(isPresented: $isSheetPresented, content: {
736 | SheetPane(locationSearchService: locationSearchService, savedLocations: savedLocations)
737 | .environmentObject(viewModel)
738 | })
739 |
740 | }
741 | }
742 |
--------------------------------------------------------------------------------