├── .gitignore
├── ReadMe.md
├── city_json
└── city_filter.py
├── img
└── capture.gif
├── sui_sample.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ └── sui_sample.xcscheme
└── sui_sample
├── Actions
├── Fetch5DaysForecast.swift
├── FetchCurrentLocation.swift
├── FilterCities.swift
└── ReadCitiesFromFile.swift
├── Assets.xcassets
├── AppIcon.appiconset
│ └── Contents.json
├── Colors
│ ├── Contents.json
│ ├── tabBgColor.colorset
│ │ └── Contents.json
│ └── tabFrameColor.colorset
│ │ └── Contents.json
├── Contents.json
└── blank.imageset
│ ├── Contents.json
│ └── blank.png
├── Base.lproj
└── LaunchScreen.storyboard
├── ColorTheme.swift
├── CommonView
├── MapView.swift
└── TabButton.swift
├── Config-sample.plist
├── Entity
├── City.swift
└── Weather.swift
├── Info.plist
├── Preview Content
└── Preview Assets.xcassets
│ └── Contents.json
├── RootView.swift
├── Stores
└── PageStore.swift
├── Utils
├── Async+Ext.swift
├── ResourceFunctions.swift
└── SwiftUI+Ext.swift
├── View
├── CityListView.swift
├── CitySelectionView.swift
├── ForecastView.swift
└── LocationSelectionView.swift
├── WeatherApp.swift
└── asset
├── 5day_sample.json
└── city_list.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ##
6 | city_json/*.json
7 | Config.plist
8 | .DS_Store
9 |
10 | ## User settings
11 | xcuserdata/
12 |
13 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
14 | *.xcscmblueprint
15 | *.xccheckout
16 |
17 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
18 | build/
19 | DerivedData/
20 | *.moved-aside
21 | *.pbxuser
22 | !default.pbxuser
23 | *.mode1v3
24 | !default.mode1v3
25 | *.mode2v3
26 | !default.mode2v3
27 | *.perspectivev3
28 | !default.perspectivev3
29 |
30 | ## Xcode Patch
31 | *.xcodeproj/*
32 | !*.xcodeproj/project.pbxproj
33 | !*.xcodeproj/xcshareddata/
34 | !*.xcworkspace/contents.xcworkspacedata
35 | /*.gcno
36 |
37 | *.entitlements
38 |
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 | # SwiftUI + Async Weather
2 |
3 | Showing Weather forecast using SwiftUI + Async.
4 |
5 | Select a area, and show daily forecasts, using `WeatherKit`
6 |
7 |
8 |
9 |
10 |
11 | ## Highlight of this app
12 |
13 | * SwiftUI.framework
14 | * Swift6 Support
15 | * Using `LocationButton`, which are new APIs in iOS 15.
16 | * MKMapView with CoreLocation
17 | * GPS with async/await
18 | * detect tap and add pin at tap position
19 | * Adapt dark mode with asset-catalog-colors
20 | * Use `WeatherKit`
21 |
22 | ## how to run
23 |
24 | * Install Xcode16.0 or later
25 | * Build and Run
26 |
27 | By default, this app will access local-json file.
28 | If you want to use network-api, please do as following
29 |
30 | * sign in [OpenWeather](https://openweathermap.org/) and create Api Key
31 | * rename `Config-sample.plist` to `Config.plist`
32 | * Write your Api Key in `Constants.plist`
33 |
34 | > city_list.json from Openweather is too big (20MB!).
35 | > So I shrinked it. Please check city_json/
36 |
37 |
38 | ### WeatherKit
39 |
40 | To run devices, you need to set up your project as follows.
41 |
42 | https://developer.apple.com/documentation/weatherkit/fetching_weather_forecasts_with_weatherkit
43 |
44 | ## file structure
45 |
46 | + /
47 | +- RootView.swift : view at app-launching
48 | +- Theme.swift : Color list
49 | +- asset/ : cities and sample json
50 | +- Actions/ : side-effects
51 | +- CommonView/ : common views
52 | +- Utils/
53 | +- View/
54 |
55 | ## app structure
56 |
57 | * Simplify structure, no view-models
58 | * `Actions/` has complex side-effect operations with `@Environment`
59 |
60 | ## thanks
61 |
62 | * [OpenWeather](https://openweathermap.org/)
63 | * Many repositories using SwiftUI and authors.
64 |
65 |
--------------------------------------------------------------------------------
/city_json/city_filter.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | # open-weather's city list file is too large. it containts about 210,000 cities.
4 | # So this program reduces data. about 1 tenth
5 | # How To use
6 | # 1. download city.list.min.json.gz from
7 | # http://bulk.openweathermap.org/sample/
8 | # 2. put it into this folder and extract it
9 | # 3. run code: python3 city_filter.py
10 | # 4. city_list.json will be created
11 | # 5. copy it to sui_sample/asset
12 |
13 | def reduce_cities():
14 | with open("city.list.min.json", "rb") as json_file:
15 | cities = json.load(json_file)
16 |
17 | reduced = list()
18 | for (i, k) in enumerate(cities):
19 | if i % 10 == 0:
20 | reduced.append(k)
21 |
22 | # if you want to filter by country, use following code
23 | reduced = [city for city in cities if city["country"] == "JP"]
24 |
25 | print("number of cities:" + str(len(reduced)))
26 | with open("city_list.json", "w") as jp_file:
27 | json.dump(reduced, jp_file)
28 |
29 |
30 | if __name__ == "__main__":
31 | reduce_cities()
32 |
--------------------------------------------------------------------------------
/img/capture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naodroid/swiftui_weather/28df70549e8808c85631f02262d48aa2f4dba1a6/img/capture.gif
--------------------------------------------------------------------------------
/sui_sample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 353B93D82DC9DA1400EED29A /* PageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93D72DC9DA1200EED29A /* PageStore.swift */; };
11 | 353B93E12DC9E08F00EED29A /* CityListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93E02DC9E08C00EED29A /* CityListView.swift */; };
12 | 353B93E32DC9E52300EED29A /* LocationSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93E22DC9E51F00EED29A /* LocationSelectionView.swift */; };
13 | 353B93E52DC9E7D100EED29A /* FetchCurrentLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93E42DC9E7CE00EED29A /* FetchCurrentLocation.swift */; };
14 | 353B93E72DC9E8D800EED29A /* ReadCitiesFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93E62DC9E8D000EED29A /* ReadCitiesFromFile.swift */; };
15 | 353B93E92DC9EDC000EED29A /* FilterCities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93E82DC9EDBC00EED29A /* FilterCities.swift */; };
16 | 353B93EF2DC9EF1400EED29A /* Fetch5DaysForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B93EE2DC9EF1000EED29A /* Fetch5DaysForecast.swift */; };
17 | 356AFCD122A617D900D472F0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 356AFCD022A617D900D472F0 /* Assets.xcassets */; };
18 | 356AFCD422A617D900D472F0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 356AFCD322A617D900D472F0 /* Preview Assets.xcassets */; };
19 | 356AFCD722A617D900D472F0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 356AFCD522A617D900D472F0 /* LaunchScreen.storyboard */; };
20 | 356AFCE322A6230800D472F0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356AFCE222A6230800D472F0 /* Weather.swift */; };
21 | 357EFDFA22A9172100432774 /* TabButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357EFDF922A9172100432774 /* TabButton.swift */; };
22 | 358804DC22A770CD00DAD63E /* SwiftUI+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358804DB22A770CD00DAD63E /* SwiftUI+Ext.swift */; };
23 | 358D999222A9D1CE00CB68C9 /* 5day_sample.json in Resources */ = {isa = PBXBuildFile; fileRef = 358D999122A9D1CE00CB68C9 /* 5day_sample.json */; };
24 | 358D999622A9D31600CB68C9 /* ResourceFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358D999522A9D31600CB68C9 /* ResourceFunctions.swift */; };
25 | 359D9E5426AA5BE800FE82AE /* WeatherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359D9E5326AA5BE800FE82AE /* WeatherApp.swift */; };
26 | 35A6311522A9D51B00940C06 /* ForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A6311422A9D51B00940C06 /* ForecastView.swift */; };
27 | 35B1506C22A6571400BC52BB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35B1506B22A6571300BC52BB /* SwiftUI.framework */; };
28 | 35B440A522A8F09B0012D6C9 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35B440A422A8F09B0012D6C9 /* CoreLocation.framework */; };
29 | 35DC39FB22A937AE00694DC0 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35DC39FA22A937AE00694DC0 /* MapKit.framework */; };
30 | 35DC39FD22A937B800694DC0 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35DC39FC22A937B800694DC0 /* MapView.swift */; };
31 | 35DC39FF22A9409B00694DC0 /* city_list.json in Resources */ = {isa = PBXBuildFile; fileRef = 35DC39FE22A9409B00694DC0 /* city_list.json */; };
32 | 35DC3A0122A9C64500694DC0 /* ColorTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35DC3A0022A9C64500694DC0 /* ColorTheme.swift */; };
33 | 35EB645322A9012200061C40 /* City.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EB645222A9012200061C40 /* City.swift */; };
34 | 35EB645B22A911F900061C40 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EB645A22A911F900061C40 /* RootView.swift */; };
35 | 35EB645D22A9120D00061C40 /* CitySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EB645C22A9120D00061C40 /* CitySelectionView.swift */; };
36 | C63E790C26A2589A00EC5CB9 /* CoreLocationUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C63E790B26A2589A00EC5CB9 /* CoreLocationUI.framework */; };
37 | C65D9D7826A25233000C94C9 /* Async+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65D9D7726A25233000C94C9 /* Async+Ext.swift */; };
38 | /* End PBXBuildFile section */
39 |
40 | /* Begin PBXFileReference section */
41 | 353B93D72DC9DA1200EED29A /* PageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageStore.swift; sourceTree = ""; };
42 | 353B93E02DC9E08C00EED29A /* CityListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityListView.swift; sourceTree = ""; };
43 | 353B93E22DC9E51F00EED29A /* LocationSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSelectionView.swift; sourceTree = ""; };
44 | 353B93E42DC9E7CE00EED29A /* FetchCurrentLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCurrentLocation.swift; sourceTree = ""; };
45 | 353B93E62DC9E8D000EED29A /* ReadCitiesFromFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadCitiesFromFile.swift; sourceTree = ""; };
46 | 353B93E82DC9EDBC00EED29A /* FilterCities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCities.swift; sourceTree = ""; };
47 | 353B93EE2DC9EF1000EED29A /* Fetch5DaysForecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetch5DaysForecast.swift; sourceTree = ""; };
48 | 3555D902295F9F1A0041AF38 /* sui_sample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = sui_sample.entitlements; sourceTree = ""; };
49 | 356AFCC722A617D700D472F0 /* sui_sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sui_sample.app; sourceTree = BUILT_PRODUCTS_DIR; };
50 | 356AFCD022A617D900D472F0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
51 | 356AFCD322A617D900D472F0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
52 | 356AFCD622A617D900D472F0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
53 | 356AFCD822A617D900D472F0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
54 | 356AFCE222A6230800D472F0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; };
55 | 357EFDF922A9172100432774 /* TabButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabButton.swift; sourceTree = ""; };
56 | 358804DB22A770CD00DAD63E /* SwiftUI+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Ext.swift"; sourceTree = ""; };
57 | 358D999122A9D1CE00CB68C9 /* 5day_sample.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 5day_sample.json; sourceTree = ""; };
58 | 358D999522A9D31600CB68C9 /* ResourceFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceFunctions.swift; sourceTree = ""; };
59 | 359D9E5326AA5BE800FE82AE /* WeatherApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherApp.swift; sourceTree = ""; };
60 | 35A6311422A9D51B00940C06 /* ForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastView.swift; sourceTree = ""; };
61 | 35B1506B22A6571300BC52BB /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
62 | 35B440A422A8F09B0012D6C9 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
63 | 35DC39FA22A937AE00694DC0 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; };
64 | 35DC39FC22A937B800694DC0 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; };
65 | 35DC39FE22A9409B00694DC0 /* city_list.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = city_list.json; sourceTree = ""; };
66 | 35DC3A0022A9C64500694DC0 /* ColorTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorTheme.swift; sourceTree = ""; };
67 | 35EB645222A9012200061C40 /* City.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = City.swift; sourceTree = ""; };
68 | 35EB645A22A911F900061C40 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; };
69 | 35EB645C22A9120D00061C40 /* CitySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CitySelectionView.swift; sourceTree = ""; };
70 | C63E790B26A2589A00EC5CB9 /* CoreLocationUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocationUI.framework; path = System/Library/Frameworks/CoreLocationUI.framework; sourceTree = SDKROOT; };
71 | C65D9D7726A25233000C94C9 /* Async+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Async+Ext.swift"; sourceTree = ""; };
72 | /* End PBXFileReference section */
73 |
74 | /* Begin PBXFrameworksBuildPhase section */
75 | 356AFCC422A617D700D472F0 /* Frameworks */ = {
76 | isa = PBXFrameworksBuildPhase;
77 | buildActionMask = 2147483647;
78 | files = (
79 | C63E790C26A2589A00EC5CB9 /* CoreLocationUI.framework in Frameworks */,
80 | 35B440A522A8F09B0012D6C9 /* CoreLocation.framework in Frameworks */,
81 | 35DC39FB22A937AE00694DC0 /* MapKit.framework in Frameworks */,
82 | 35B1506C22A6571400BC52BB /* SwiftUI.framework in Frameworks */,
83 | );
84 | runOnlyForDeploymentPostprocessing = 0;
85 | };
86 | /* End PBXFrameworksBuildPhase section */
87 |
88 | /* Begin PBXGroup section */
89 | 353B93D62DC9DA0B00EED29A /* Stores */ = {
90 | isa = PBXGroup;
91 | children = (
92 | 353B93D72DC9DA1200EED29A /* PageStore.swift */,
93 | );
94 | path = Stores;
95 | sourceTree = "";
96 | };
97 | 353B93D92DC9DBAD00EED29A /* Actions */ = {
98 | isa = PBXGroup;
99 | children = (
100 | 353B93EE2DC9EF1000EED29A /* Fetch5DaysForecast.swift */,
101 | 353B93E82DC9EDBC00EED29A /* FilterCities.swift */,
102 | 353B93E62DC9E8D000EED29A /* ReadCitiesFromFile.swift */,
103 | 353B93E42DC9E7CE00EED29A /* FetchCurrentLocation.swift */,
104 | );
105 | path = Actions;
106 | sourceTree = "";
107 | };
108 | 356AFCBE22A617D700D472F0 = {
109 | isa = PBXGroup;
110 | children = (
111 | 356AFCC922A617D700D472F0 /* sui_sample */,
112 | 356AFCC822A617D700D472F0 /* Products */,
113 | 35B1506A22A6571300BC52BB /* Frameworks */,
114 | );
115 | sourceTree = "";
116 | };
117 | 356AFCC822A617D700D472F0 /* Products */ = {
118 | isa = PBXGroup;
119 | children = (
120 | 356AFCC722A617D700D472F0 /* sui_sample.app */,
121 | );
122 | name = Products;
123 | sourceTree = "";
124 | };
125 | 356AFCC922A617D700D472F0 /* sui_sample */ = {
126 | isa = PBXGroup;
127 | children = (
128 | 3555D902295F9F1A0041AF38 /* sui_sample.entitlements */,
129 | 35B440AA22A8F6AC0012D6C9 /* asset */,
130 | 353B93D92DC9DBAD00EED29A /* Actions */,
131 | 358804DE22A772A100DAD63E /* CommonView */,
132 | 356AFCDE22A61C3C00D472F0 /* Entity */,
133 | 353B93D62DC9DA0B00EED29A /* Stores */,
134 | 356AFCE122A61DF800D472F0 /* View */,
135 | 35B1507722A67CE800BC52BB /* Utils */,
136 | 35EB645A22A911F900061C40 /* RootView.swift */,
137 | 359D9E5326AA5BE800FE82AE /* WeatherApp.swift */,
138 | 35DC3A0022A9C64500694DC0 /* ColorTheme.swift */,
139 | 356AFCD022A617D900D472F0 /* Assets.xcassets */,
140 | 356AFCD522A617D900D472F0 /* LaunchScreen.storyboard */,
141 | 356AFCD822A617D900D472F0 /* Info.plist */,
142 | 356AFCD222A617D900D472F0 /* Preview Content */,
143 | );
144 | path = sui_sample;
145 | sourceTree = "";
146 | };
147 | 356AFCD222A617D900D472F0 /* Preview Content */ = {
148 | isa = PBXGroup;
149 | children = (
150 | 356AFCD322A617D900D472F0 /* Preview Assets.xcassets */,
151 | );
152 | path = "Preview Content";
153 | sourceTree = "";
154 | };
155 | 356AFCDE22A61C3C00D472F0 /* Entity */ = {
156 | isa = PBXGroup;
157 | children = (
158 | 35EB645222A9012200061C40 /* City.swift */,
159 | 356AFCE222A6230800D472F0 /* Weather.swift */,
160 | );
161 | path = Entity;
162 | sourceTree = "";
163 | };
164 | 356AFCE122A61DF800D472F0 /* View */ = {
165 | isa = PBXGroup;
166 | children = (
167 | 35EB645C22A9120D00061C40 /* CitySelectionView.swift */,
168 | 353B93E02DC9E08C00EED29A /* CityListView.swift */,
169 | 353B93E22DC9E51F00EED29A /* LocationSelectionView.swift */,
170 | 35A6311422A9D51B00940C06 /* ForecastView.swift */,
171 | );
172 | path = View;
173 | sourceTree = "";
174 | };
175 | 358804DE22A772A100DAD63E /* CommonView */ = {
176 | isa = PBXGroup;
177 | children = (
178 | 357EFDF922A9172100432774 /* TabButton.swift */,
179 | 35DC39FC22A937B800694DC0 /* MapView.swift */,
180 | );
181 | path = CommonView;
182 | sourceTree = "";
183 | };
184 | 35B1506A22A6571300BC52BB /* Frameworks */ = {
185 | isa = PBXGroup;
186 | children = (
187 | C63E790B26A2589A00EC5CB9 /* CoreLocationUI.framework */,
188 | 35DC39FA22A937AE00694DC0 /* MapKit.framework */,
189 | 35B440A422A8F09B0012D6C9 /* CoreLocation.framework */,
190 | 35B1506B22A6571300BC52BB /* SwiftUI.framework */,
191 | );
192 | name = Frameworks;
193 | sourceTree = "";
194 | };
195 | 35B1507722A67CE800BC52BB /* Utils */ = {
196 | isa = PBXGroup;
197 | children = (
198 | C65D9D7726A25233000C94C9 /* Async+Ext.swift */,
199 | 358804DB22A770CD00DAD63E /* SwiftUI+Ext.swift */,
200 | 358D999522A9D31600CB68C9 /* ResourceFunctions.swift */,
201 | );
202 | path = Utils;
203 | sourceTree = "";
204 | };
205 | 35B440AA22A8F6AC0012D6C9 /* asset */ = {
206 | isa = PBXGroup;
207 | children = (
208 | 358D999122A9D1CE00CB68C9 /* 5day_sample.json */,
209 | 35DC39FE22A9409B00694DC0 /* city_list.json */,
210 | );
211 | path = asset;
212 | sourceTree = "";
213 | };
214 | /* End PBXGroup section */
215 |
216 | /* Begin PBXNativeTarget section */
217 | 356AFCC622A617D700D472F0 /* sui_sample */ = {
218 | isa = PBXNativeTarget;
219 | buildConfigurationList = 356AFCDB22A617D900D472F0 /* Build configuration list for PBXNativeTarget "sui_sample" */;
220 | buildPhases = (
221 | 356AFCC322A617D700D472F0 /* Sources */,
222 | 356AFCC422A617D700D472F0 /* Frameworks */,
223 | 356AFCC522A617D700D472F0 /* Resources */,
224 | );
225 | buildRules = (
226 | );
227 | dependencies = (
228 | );
229 | name = sui_sample;
230 | productName = sui_sample;
231 | productReference = 356AFCC722A617D700D472F0 /* sui_sample.app */;
232 | productType = "com.apple.product-type.application";
233 | };
234 | /* End PBXNativeTarget section */
235 |
236 | /* Begin PBXProject section */
237 | 356AFCBF22A617D700D472F0 /* Project object */ = {
238 | isa = PBXProject;
239 | attributes = {
240 | LastSwiftUpdateCheck = 1100;
241 | LastUpgradeCheck = 1300;
242 | ORGANIZATIONNAME = naodroid;
243 | TargetAttributes = {
244 | 356AFCC622A617D700D472F0 = {
245 | CreatedOnToolsVersion = 11.0;
246 | };
247 | };
248 | };
249 | buildConfigurationList = 356AFCC222A617D700D472F0 /* Build configuration list for PBXProject "sui_sample" */;
250 | compatibilityVersion = "Xcode 9.3";
251 | developmentRegion = en;
252 | hasScannedForEncodings = 0;
253 | knownRegions = (
254 | en,
255 | Base,
256 | );
257 | mainGroup = 356AFCBE22A617D700D472F0;
258 | productRefGroup = 356AFCC822A617D700D472F0 /* Products */;
259 | projectDirPath = "";
260 | projectRoot = "";
261 | targets = (
262 | 356AFCC622A617D700D472F0 /* sui_sample */,
263 | );
264 | };
265 | /* End PBXProject section */
266 |
267 | /* Begin PBXResourcesBuildPhase section */
268 | 356AFCC522A617D700D472F0 /* Resources */ = {
269 | isa = PBXResourcesBuildPhase;
270 | buildActionMask = 2147483647;
271 | files = (
272 | 356AFCD722A617D900D472F0 /* LaunchScreen.storyboard in Resources */,
273 | 356AFCD422A617D900D472F0 /* Preview Assets.xcassets in Resources */,
274 | 35DC39FF22A9409B00694DC0 /* city_list.json in Resources */,
275 | 356AFCD122A617D900D472F0 /* Assets.xcassets in Resources */,
276 | 358D999222A9D1CE00CB68C9 /* 5day_sample.json in Resources */,
277 | );
278 | runOnlyForDeploymentPostprocessing = 0;
279 | };
280 | /* End PBXResourcesBuildPhase section */
281 |
282 | /* Begin PBXSourcesBuildPhase section */
283 | 356AFCC322A617D700D472F0 /* Sources */ = {
284 | isa = PBXSourcesBuildPhase;
285 | buildActionMask = 2147483647;
286 | files = (
287 | 35EB645D22A9120D00061C40 /* CitySelectionView.swift in Sources */,
288 | 35DC39FD22A937B800694DC0 /* MapView.swift in Sources */,
289 | 353B93E32DC9E52300EED29A /* LocationSelectionView.swift in Sources */,
290 | 359D9E5426AA5BE800FE82AE /* WeatherApp.swift in Sources */,
291 | 35A6311522A9D51B00940C06 /* ForecastView.swift in Sources */,
292 | C65D9D7826A25233000C94C9 /* Async+Ext.swift in Sources */,
293 | 35DC3A0122A9C64500694DC0 /* ColorTheme.swift in Sources */,
294 | 35EB645B22A911F900061C40 /* RootView.swift in Sources */,
295 | 353B93E52DC9E7D100EED29A /* FetchCurrentLocation.swift in Sources */,
296 | 35EB645322A9012200061C40 /* City.swift in Sources */,
297 | 353B93D82DC9DA1400EED29A /* PageStore.swift in Sources */,
298 | 358D999622A9D31600CB68C9 /* ResourceFunctions.swift in Sources */,
299 | 358804DC22A770CD00DAD63E /* SwiftUI+Ext.swift in Sources */,
300 | 356AFCE322A6230800D472F0 /* Weather.swift in Sources */,
301 | 353B93E12DC9E08F00EED29A /* CityListView.swift in Sources */,
302 | 353B93EF2DC9EF1400EED29A /* Fetch5DaysForecast.swift in Sources */,
303 | 353B93E92DC9EDC000EED29A /* FilterCities.swift in Sources */,
304 | 353B93E72DC9E8D800EED29A /* ReadCitiesFromFile.swift in Sources */,
305 | 357EFDFA22A9172100432774 /* TabButton.swift in Sources */,
306 | );
307 | runOnlyForDeploymentPostprocessing = 0;
308 | };
309 | /* End PBXSourcesBuildPhase section */
310 |
311 | /* Begin PBXVariantGroup section */
312 | 356AFCD522A617D900D472F0 /* LaunchScreen.storyboard */ = {
313 | isa = PBXVariantGroup;
314 | children = (
315 | 356AFCD622A617D900D472F0 /* Base */,
316 | );
317 | name = LaunchScreen.storyboard;
318 | sourceTree = "";
319 | };
320 | /* End PBXVariantGroup section */
321 |
322 | /* Begin XCBuildConfiguration section */
323 | 356AFCD922A617D900D472F0 /* Debug */ = {
324 | isa = XCBuildConfiguration;
325 | buildSettings = {
326 | ALWAYS_SEARCH_USER_PATHS = NO;
327 | CLANG_ANALYZER_NONNULL = YES;
328 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
330 | CLANG_CXX_LIBRARY = "libc++";
331 | CLANG_ENABLE_MODULES = YES;
332 | CLANG_ENABLE_OBJC_ARC = YES;
333 | CLANG_ENABLE_OBJC_WEAK = YES;
334 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
335 | CLANG_WARN_BOOL_CONVERSION = YES;
336 | CLANG_WARN_COMMA = YES;
337 | CLANG_WARN_CONSTANT_CONVERSION = YES;
338 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
339 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
340 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
341 | CLANG_WARN_EMPTY_BODY = YES;
342 | CLANG_WARN_ENUM_CONVERSION = YES;
343 | CLANG_WARN_INFINITE_RECURSION = YES;
344 | CLANG_WARN_INT_CONVERSION = YES;
345 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
346 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
347 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
348 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
349 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
350 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
351 | CLANG_WARN_STRICT_PROTOTYPES = YES;
352 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
353 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
354 | CLANG_WARN_UNREACHABLE_CODE = YES;
355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
356 | COPY_PHASE_STRIP = NO;
357 | DEBUG_INFORMATION_FORMAT = dwarf;
358 | ENABLE_STRICT_OBJC_MSGSEND = YES;
359 | ENABLE_TESTABILITY = YES;
360 | GCC_C_LANGUAGE_STANDARD = gnu11;
361 | GCC_DYNAMIC_NO_PIC = NO;
362 | GCC_NO_COMMON_BLOCKS = YES;
363 | GCC_OPTIMIZATION_LEVEL = 0;
364 | GCC_PREPROCESSOR_DEFINITIONS = (
365 | "DEBUG=1",
366 | "$(inherited)",
367 | );
368 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
369 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
370 | GCC_WARN_UNDECLARED_SELECTOR = YES;
371 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
372 | GCC_WARN_UNUSED_FUNCTION = YES;
373 | GCC_WARN_UNUSED_VARIABLE = YES;
374 | IPHONEOS_DEPLOYMENT_TARGET = 17.6;
375 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
376 | MTL_FAST_MATH = YES;
377 | ONLY_ACTIVE_ARCH = YES;
378 | SDKROOT = iphoneos;
379 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
380 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
381 | SWIFT_VERSION = 6.0;
382 | };
383 | name = Debug;
384 | };
385 | 356AFCDA22A617D900D472F0 /* Release */ = {
386 | isa = XCBuildConfiguration;
387 | buildSettings = {
388 | ALWAYS_SEARCH_USER_PATHS = NO;
389 | CLANG_ANALYZER_NONNULL = YES;
390 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
391 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
392 | CLANG_CXX_LIBRARY = "libc++";
393 | CLANG_ENABLE_MODULES = YES;
394 | CLANG_ENABLE_OBJC_ARC = YES;
395 | CLANG_ENABLE_OBJC_WEAK = YES;
396 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
397 | CLANG_WARN_BOOL_CONVERSION = YES;
398 | CLANG_WARN_COMMA = YES;
399 | CLANG_WARN_CONSTANT_CONVERSION = YES;
400 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
401 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
402 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
403 | CLANG_WARN_EMPTY_BODY = YES;
404 | CLANG_WARN_ENUM_CONVERSION = YES;
405 | CLANG_WARN_INFINITE_RECURSION = YES;
406 | CLANG_WARN_INT_CONVERSION = YES;
407 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
408 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
409 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
411 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
412 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
413 | CLANG_WARN_STRICT_PROTOTYPES = YES;
414 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
415 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
416 | CLANG_WARN_UNREACHABLE_CODE = YES;
417 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
418 | COPY_PHASE_STRIP = NO;
419 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
420 | ENABLE_NS_ASSERTIONS = NO;
421 | ENABLE_STRICT_OBJC_MSGSEND = YES;
422 | GCC_C_LANGUAGE_STANDARD = gnu11;
423 | GCC_NO_COMMON_BLOCKS = YES;
424 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
425 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
426 | GCC_WARN_UNDECLARED_SELECTOR = YES;
427 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
428 | GCC_WARN_UNUSED_FUNCTION = YES;
429 | GCC_WARN_UNUSED_VARIABLE = YES;
430 | IPHONEOS_DEPLOYMENT_TARGET = 17.6;
431 | MTL_ENABLE_DEBUG_INFO = NO;
432 | MTL_FAST_MATH = YES;
433 | SDKROOT = iphoneos;
434 | SWIFT_COMPILATION_MODE = wholemodule;
435 | SWIFT_OPTIMIZATION_LEVEL = "-O";
436 | SWIFT_VERSION = 6.0;
437 | VALIDATE_PRODUCT = YES;
438 | };
439 | name = Release;
440 | };
441 | 356AFCDC22A617D900D472F0 /* Debug */ = {
442 | isa = XCBuildConfiguration;
443 | buildSettings = {
444 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
445 | CODE_SIGN_ENTITLEMENTS = sui_sample/sui_sample.entitlements;
446 | CODE_SIGN_IDENTITY = "Apple Development";
447 | CODE_SIGN_STYLE = Automatic;
448 | DEVELOPMENT_ASSET_PATHS = "sui_sample/Preview\\ Content";
449 | DEVELOPMENT_TEAM = 5JHB3Z4BRX;
450 | ENABLE_PREVIEWS = YES;
451 | INFOPLIST_FILE = sui_sample/Info.plist;
452 | LD_RUNPATH_SEARCH_PATHS = (
453 | "$(inherited)",
454 | "@executable_path/Frameworks",
455 | );
456 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.naodroid.sui-sample";
457 | PRODUCT_NAME = "$(TARGET_NAME)";
458 | PROVISIONING_PROFILE_SPECIFIER = "";
459 | TARGETED_DEVICE_FAMILY = "1,2";
460 | };
461 | name = Debug;
462 | };
463 | 356AFCDD22A617D900D472F0 /* Release */ = {
464 | isa = XCBuildConfiguration;
465 | buildSettings = {
466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
467 | CODE_SIGN_ENTITLEMENTS = sui_sample/sui_sample.entitlements;
468 | CODE_SIGN_IDENTITY = "Apple Development";
469 | CODE_SIGN_STYLE = Automatic;
470 | DEVELOPMENT_ASSET_PATHS = "sui_sample/Preview\\ Content";
471 | DEVELOPMENT_TEAM = 5JHB3Z4BRX;
472 | ENABLE_PREVIEWS = YES;
473 | INFOPLIST_FILE = sui_sample/Info.plist;
474 | LD_RUNPATH_SEARCH_PATHS = (
475 | "$(inherited)",
476 | "@executable_path/Frameworks",
477 | );
478 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.naodroid.sui-sample";
479 | PRODUCT_NAME = "$(TARGET_NAME)";
480 | PROVISIONING_PROFILE_SPECIFIER = "";
481 | TARGETED_DEVICE_FAMILY = "1,2";
482 | };
483 | name = Release;
484 | };
485 | /* End XCBuildConfiguration section */
486 |
487 | /* Begin XCConfigurationList section */
488 | 356AFCC222A617D700D472F0 /* Build configuration list for PBXProject "sui_sample" */ = {
489 | isa = XCConfigurationList;
490 | buildConfigurations = (
491 | 356AFCD922A617D900D472F0 /* Debug */,
492 | 356AFCDA22A617D900D472F0 /* Release */,
493 | );
494 | defaultConfigurationIsVisible = 0;
495 | defaultConfigurationName = Release;
496 | };
497 | 356AFCDB22A617D900D472F0 /* Build configuration list for PBXNativeTarget "sui_sample" */ = {
498 | isa = XCConfigurationList;
499 | buildConfigurations = (
500 | 356AFCDC22A617D900D472F0 /* Debug */,
501 | 356AFCDD22A617D900D472F0 /* Release */,
502 | );
503 | defaultConfigurationIsVisible = 0;
504 | defaultConfigurationName = Release;
505 | };
506 | /* End XCConfigurationList section */
507 | };
508 | rootObject = 356AFCBF22A617D700D472F0 /* Project object */;
509 | }
510 |
--------------------------------------------------------------------------------
/sui_sample.xcodeproj/xcshareddata/xcschemes/sui_sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/sui_sample/Actions/Fetch5DaysForecast.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FetchWeatherForecast.swift
3 | // sui_sample
4 | //
5 | // Created by nao on 2025/05/06.
6 | // Copyright © 2025 naodroid. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import CoreLocation
11 | import WeatherKit
12 |
13 | extension EnvironmentValues {
14 | @Entry var fetch5DaysForecast = Fetch5DaysForecast()
15 | }
16 |
17 | struct Fetch5DaysForecast {
18 | func callAsFunction(city: City) async throws -> [DayForecast] {
19 | try await self(lat: city.coord.lat, lon: city.coord.lon)
20 | }
21 | func callAsFunction(lat: Double, lon: Double) async throws -> [DayForecast] {
22 | let service = WeatherService()
23 | let loc = CLLocation(latitude: lat, longitude: lon)
24 | let cal = Calendar.current
25 | let startDate = Date()
26 | let endDate = cal.date(byAdding: .day, value: 5, to: startDate)!
27 | // fetch daily
28 | let query1 = WeatherQuery.daily(startDate: startDate, endDate: endDate)
29 | let daily = try await service.weather(for: loc, including: query1)
30 | // fetch hourly
31 | let query2 = WeatherQuery.hourly(startDate: startDate, endDate: endDate)
32 | let hourly = try await service.weather(for: loc, including: query2)
33 |
34 | // combine daily with hourly
35 | var results = daily.map {
36 | DayForecast(forecast: $0)
37 | }
38 | for h1 in hourly {
39 | let h2 = HourForecast(forecast: h1)
40 | let idx = results.firstIndex {
41 | $0.yyyymmdd == h2.yyyymmdd
42 | }
43 | if let idx {
44 | results[idx].hourly.append(h2)
45 | }
46 | }
47 | return results
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/sui_sample/Actions/FetchCurrentLocation.swift:
--------------------------------------------------------------------------------
1 | @preconcurrency import CoreLocation
2 | //
3 | // LocationActions.swift
4 | // sui_sample
5 | //
6 | // Created by nao on 2025/05/06.
7 | // Copyright © 2025 naodroid. All rights reserved.
8 | //
9 | import Foundation
10 | import SwiftUI
11 |
12 | /// define errors
13 | enum LocationError: Error {
14 | case serviceNotAvailable
15 | case permissionDenied
16 | case canceled
17 | }
18 | typealias LocationContinuation = CheckedContinuation
19 |
20 | extension EnvironmentValues {
21 | @Entry var fetchCurrentLocation = FetchCurrentLocation()
22 | }
23 | /// fetch gps location
24 | @MainActor
25 | final class FetchCurrentLocation {
26 | private var handler: LocationHandler?
27 |
28 | nonisolated init() {
29 | }
30 | func callAsFunction() async throws -> CLLocation {
31 | let handler = self.handler ?? LocationHandler()
32 | self.handler = handler
33 | return try await handler.fetchCurrentLocation()
34 | }
35 | }
36 |
37 | /// Event Handling class,
38 | /// CLLocation requires NSObject
39 | @MainActor
40 | private class LocationHandler: NSObject, CLLocationManagerDelegate {
41 | //other
42 | private var locationManager: CLLocationManager?
43 | private var continuation: LocationContinuation?
44 | //
45 | func fetchCurrentLocation() async throws -> CLLocation {
46 | let mn = self.locationManager ?? CLLocationManager()
47 | self.locationManager = mn
48 | mn.delegate = self
49 | if let c = self.continuation {
50 | c.resume(throwing: LocationError.canceled)
51 | self.continuation = nil
52 | }
53 | return try await withCheckedThrowingContinuation {
54 | (continuation: LocationContinuation) in
55 | self.continuation = continuation
56 | mn.requestLocation()
57 | }
58 | }
59 |
60 | func cancel() {
61 | self.locationManager?.stopUpdatingLocation()
62 | self.locationManager = nil
63 | if let c = self.continuation {
64 | c.resume(throwing: LocationError.canceled)
65 | self.continuation = nil
66 | }
67 | }
68 |
69 | // MARK: Delegate
70 | nonisolated func locationManager(
71 | _ manager: CLLocationManager,
72 | didChangeAuthorization status: CLAuthorizationStatus
73 | ) {
74 | Task {
75 | await didChangeAuthorization(status)
76 | }
77 | }
78 | private func didChangeAuthorization(_ status: CLAuthorizationStatus) {
79 | switch status {
80 | case .notDetermined:
81 | //If using a LocationButton, this code will not run.
82 | self.locationManager?.requestWhenInUseAuthorization()
83 | case .restricted, .denied:
84 | self.continuation?.resume(throwing: LocationError.permissionDenied)
85 | self.continuation = nil
86 | case .authorizedAlways, .authorizedWhenInUse:
87 | break
88 | @unknown default:
89 | break
90 | }
91 | }
92 | nonisolated func locationManager(
93 | _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]
94 | ) {
95 | Task {
96 | await didUpdateLocations(locations)
97 | }
98 | }
99 | private func didUpdateLocations(_ locations: [CLLocation]) {
100 | guard let location = locations.first else {
101 | return
102 | }
103 | self.continuation?.resume(returning: location)
104 | self.continuation = nil
105 | }
106 | nonisolated func locationManager(
107 | _ manager: CLLocationManager, didFailWithError error: Error
108 | ) {
109 | Task {
110 | await didFailWithError(error)
111 | }
112 | }
113 | private func didFailWithError(_ error: Error) {
114 | self.continuation?.resume(throwing: error)
115 | self.continuation = nil
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/sui_sample/Actions/FilterCities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilterCities.swift
3 | // sui_sample
4 | //
5 | // Created by nao on 2025/05/06.
6 | // Copyright © 2025 naodroid. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension EnvironmentValues {
12 | @Entry var filterCities = FilterCities()
13 | }
14 |
15 | struct FilterCities {
16 | func callAsFunction(
17 | keyword: String,
18 | allCities: [City]
19 | ) async -> [City] {
20 | let k = keyword.lowercased().trimmingCharacters(
21 | in: .whitespacesAndNewlines)
22 | let result = allCities.filter { (c) -> Bool in
23 | return keyword.isEmpty || c.name.lowercased().contains(k)
24 | }
25 | let limited =
26 | (result.count >= 100)
27 | ? Array(result[..<100])
28 | : result
29 | return limited
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sui_sample/Actions/ReadCitiesFromFile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReadCitiesFromFile.swift
3 | // sui_sample
4 | //
5 | // Created by nao on 2025/05/06.
6 | // Copyright © 2025 naodroid. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// define errors
12 | enum CityListError: Error {
13 | case resourceNotFount
14 | case parseError
15 | }
16 | /// ext
17 | extension EnvironmentValues {
18 | @Entry var readCitiesFromFile = ReadCitiesFromFile()
19 | }
20 |
21 | /// read city names from resource the json file
22 | struct ReadCitiesFromFile {
23 | /// read all cities from json
24 | func callAsFunction() async throws -> [City] {
25 | guard let path = Bundle.main.path(forResource: "city_list.json", ofType: nil),
26 | let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
27 | print("Json read error. Please put city_list.json to asset folder.")
28 | throw CityListError.resourceNotFount
29 | }
30 | do {
31 | let cities = try JSONDecoder().decode([City].self, from: data)
32 | return cities
33 | } catch {
34 | throw CityListError.parseError
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/Colors/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/Colors/tabBgColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.678",
13 | "alpha" : "1.000",
14 | "blue" : "1.000",
15 | "green" : "0.933"
16 | }
17 | }
18 | },
19 | {
20 | "idiom" : "universal",
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "dark"
25 | }
26 | ],
27 | "color" : {
28 | "color-space" : "srgb",
29 | "components" : {
30 | "red" : "0.239",
31 | "alpha" : "1.000",
32 | "blue" : "0.544",
33 | "green" : "0.500"
34 | }
35 | }
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/Colors/tabFrameColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.374",
13 | "alpha" : "1.000",
14 | "blue" : "1.000",
15 | "green" : "0.843"
16 | }
17 | }
18 | },
19 | {
20 | "idiom" : "universal",
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "dark"
25 | }
26 | ],
27 | "color" : {
28 | "color-space" : "srgb",
29 | "components" : {
30 | "red" : "0.082",
31 | "alpha" : "1.000",
32 | "blue" : "0.411",
33 | "green" : "0.411"
34 | }
35 | }
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/blank.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "blank.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/sui_sample/Assets.xcassets/blank.imageset/blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naodroid/swiftui_weather/28df70549e8808c85631f02262d48aa2f4dba1a6/sui_sample/Assets.xcassets/blank.imageset/blank.png
--------------------------------------------------------------------------------
/sui_sample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/sui_sample/ColorTheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorTheme.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/07.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 | import Combine
12 |
13 | // TO dispatch dark-mode-echanging, create ownVC and dispatch event through Observer
14 | // I haven't found other methods to do same thing.
15 | class TraitObservingViewController: UIHostingController where T: View {
16 | var theme: ColorTheme?
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | theme?.currentTrait = self.traitCollection
21 | }
22 |
23 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
24 | self.theme?.currentTrait = self.traitCollection
25 | }
26 | }
27 |
28 | extension EnvironmentValues {
29 | @Entry var colorTheme = ColorTheme()
30 | }
31 |
32 |
33 |
34 | @MainActor
35 | @Observable
36 | final class ColorTheme {
37 | let didChange = PassthroughSubject()
38 | fileprivate var currentTrait: UITraitCollection? {
39 | didSet {
40 | self.didChange.send(self)
41 | }
42 | }
43 |
44 | nonisolated init() {
45 | }
46 |
47 | private func resolve(_ color: UIColor) -> Color {
48 | guard let t = self.currentTrait else {
49 | return color.toSwiftUI()
50 | }
51 | return color.resolvedColor(with: t).toSwiftUI()
52 | }
53 |
54 |
55 | /// this color works when you switch dark-mode.
56 | /// but, the color will be reset to light-color after chaning tab
57 | /// .colorScheme(.dark) also won't work. so I created resolve function
58 | var tabBgColor: Color {
59 | self.resolve(UIColor(named: "tabBgColor")!)
60 | }
61 | var tabFrameColor: Color {
62 | self.resolve(UIColor(named: "tabFrameColor")!)
63 | }
64 | var foregronud: Color {
65 | self.resolve(UIColor.label)
66 | }
67 | var foregronudLight: Color {
68 | self.resolve(UIColor.secondaryLabel)
69 | }
70 | var foregronudReversed: Color {
71 | self.resolve(UIColor.tertiaryLabel)
72 | }
73 | var background: Color {
74 | self.resolve(UIColor.systemBackground)
75 | }
76 | }
77 | //convert to SwiftUI-Color
78 | extension UIColor {
79 | func toSwiftUI() -> Color {
80 | var r = CGFloat(1.0)
81 | var g = CGFloat(1.0)
82 | var b = CGFloat(1.0)
83 | var a = CGFloat(1.0)
84 | self.getRed(&r, green: &g, blue: &b, alpha: &a)
85 |
86 | //TODO: set correct ColorSpace
87 | return Color(Color.RGBColorSpace.sRGB,
88 | red: Double(r),
89 | green: Double(g),
90 | blue: Double(b),
91 | opacity: Double(a))
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/sui_sample/CommonView/MapView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapView.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/06.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import MapKit
11 |
12 | @MainActor
13 | struct MapView: UIViewRepresentable {
14 |
15 | let loc: CLLocation?
16 | let pin: CLLocationCoordinate2D?
17 | let onTap: ((CLLocationCoordinate2D) -> Void)?
18 |
19 | func makeCoordinator() -> Coordinator {
20 | Coordinator(self)
21 | }
22 |
23 | func makeUIView(context: Context) -> MKMapView {
24 | let map = MKMapView(frame: .zero)
25 | context.coordinator.addGesture(map: map)
26 | map.isZoomEnabled = true
27 | return map
28 | }
29 |
30 | func updateUIView(_ view: MKMapView, context: Context) {
31 |
32 | let coordinator = context.coordinator
33 | if let loc = self.loc, loc != coordinator.lastCenterLocation {
34 | let coordinate = loc.coordinate
35 | let span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
36 | let region = MKCoordinateRegion(center: coordinate, span: span)
37 | view.setRegion(region, animated: true)
38 | }
39 | coordinator.lastCenterLocation = self.loc
40 | //
41 | if let loc = self.pin, loc != coordinator.lastPinLocation {
42 | for a in view.annotations {
43 | view.removeAnnotation(a)
44 | }
45 |
46 | let annotation = MKPointAnnotation()
47 | annotation.coordinate = loc
48 | view.addAnnotation(annotation)
49 | }
50 | coordinator.lastPinLocation = self.pin
51 | }
52 | @MainActor
53 | class Coordinator: NSObject {
54 | var parent: MapView
55 | var lastCenterLocation: CLLocation? = nil
56 | var lastPinLocation: CLLocationCoordinate2D? = nil
57 |
58 | init(_ parent: MapView) {
59 | self.parent = parent
60 | }
61 | func addGesture(map: MKMapView) {
62 | let gesture = UITapGestureRecognizer(target: self, action: #selector(didTapMap(_:)))
63 | map.addGestureRecognizer(gesture)
64 | }
65 | @objc func didTapMap(_ sender: UITapGestureRecognizer) {
66 | guard let map = sender.view as? MKMapView else {
67 | return
68 | }
69 | let point = sender.location(in: map)
70 | let pos = map.convert(point, toCoordinateFrom: map)
71 | self.parent.onTap?(pos)
72 | }
73 | }
74 | }
75 | extension CLLocationCoordinate2D: @retroactive Equatable {
76 | public static func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
77 | lhs.latitude == rhs.latitude
78 | && lhs.longitude == rhs.longitude
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/sui_sample/CommonView/TabButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabButton.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/06.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct TabButton: View {
13 | // dependencies
14 | @Environment(\.colorTheme) var colorTheme
15 | // params
16 | let text: String
17 | let selected: Bool
18 | let action: () -> Void
19 |
20 | private var fillColorTop: Color {
21 | selected ? colorTheme.tabFrameColor : colorTheme.background
22 | }
23 | private var fillColorBottom: Color {
24 | selected ? colorTheme.tabBgColor : colorTheme.background
25 | }
26 | private var textColor: Color {
27 | selected ? colorTheme.foregronud : colorTheme.foregronudLight
28 | }
29 |
30 | var body: some View {
31 | Button(action: action) {
32 | Text(text)
33 | .foregroundColor(colorTheme.foregronud)
34 | .background {
35 | RoundedBg(
36 | strokeColor: colorTheme.tabFrameColor,
37 | fillColorTop: fillColorTop,
38 | fillColorBottom: fillColorBottom
39 | )
40 | .colorScheme(.light)
41 | }
42 | .frame(height: 36.0)
43 | }
44 | }
45 | }
46 |
47 | struct RoundedBg: View {
48 | // params
49 | let strokeColor: Color
50 | let fillColorTop: Color
51 | let fillColorBottom: Color
52 | private let cornerRadius: CGFloat = 8.0
53 |
54 | var body: some View {
55 | GeometryReader { geometory in
56 | ZStack {
57 | self.createPath(for: geometory).fill(
58 | LinearGradient(
59 | gradient: .init(colors: [
60 | self.fillColorTop, self.fillColorBottom,
61 | ]),
62 | startPoint: .init(x: 0.5, y: 0),
63 | endPoint: .init(x: 0.5, y: 1.0)
64 | ))
65 | self.createPath(for: geometory).stroke(self.strokeColor)
66 | }
67 | }
68 | }
69 | private func createPath(for geometory: GeometryProxy) -> Path {
70 | Path { path in
71 | path.move(
72 | to: CGPoint(
73 | x: 0,
74 | y: geometory.size.height
75 | ))
76 | path.addLine(
77 | to: CGPoint(
78 | x: 0,
79 | y: self.cornerRadius
80 | ))
81 | path.addQuadCurve(
82 | to: CGPoint(
83 | x: self.cornerRadius,
84 | y: 0
85 | ),
86 | control: CGPoint(
87 | x: 0,
88 | y: 0
89 | )
90 | )
91 | path.addLine(
92 | to: CGPoint(
93 | x: geometory.size.width - self.cornerRadius,
94 | y: 0
95 | ))
96 | path.addQuadCurve(
97 | to: CGPoint(
98 | x: geometory.size.width,
99 | y: self.cornerRadius
100 | ),
101 | control: CGPoint(
102 | x: geometory.size.width,
103 | y: 0
104 | )
105 | )
106 | path.addLine(
107 | to: CGPoint(
108 | x: geometory.size.width,
109 | y: geometory.size.height
110 | ))
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/sui_sample/Config-sample.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OpenWeatherApiKey
6 | WriteYourKey
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sui_sample/Entity/City.swift:
--------------------------------------------------------------------------------
1 | //
2 | // City.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/06.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | /// open weather city data
13 | /// sample json
14 | /// [{"country": "UA", "id": 707860, "coord": {"lat": 44.549999, "lon": 34.283333}, "name": "Hurzuf"}]
15 |
16 |
17 | struct City: Codable, Equatable, Identifiable, Hashable {
18 | let id: Int64
19 | let name: String
20 | let country: String
21 | let coord: Coord
22 | }
23 | struct Coord: Codable, Equatable, Hashable {
24 | let lat: Double
25 | let lon: Double
26 | }
27 |
--------------------------------------------------------------------------------
/sui_sample/Entity/Weather.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Weather.swift
3 | // sui_sample
4 | //
5 | // Created by nadroid on 2019/06/04.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @preconcurrency import WeatherKit
11 |
12 | /// wrap weatherkit's struct to confirm identifiable & Sendable
13 | struct DayForecast: Identifiable, Sendable {
14 | var id: String { yyyymmdd }
15 | let yyyymmdd: String //YYYYMMDD
16 | let forecast: DayWeather
17 | var hourly: [HourForecast] = []
18 |
19 | init(forecast: DayWeather) {
20 | self.yyyymmdd = forecast.date.yyyymmdd
21 | self.forecast = forecast
22 | }
23 | }
24 |
25 | struct HourForecast: Identifiable, Sendable {
26 | let id: String
27 | let yyyymmdd: String
28 | let hhmm: String
29 | let forecast: HourWeather
30 | init(forecast: HourWeather) {
31 | self.id = "\(forecast.date)"
32 | self.yyyymmdd = forecast.date.yyyymmdd
33 | self.hhmm = forecast.date.hhmm
34 | self.forecast = forecast
35 | }
36 | }
37 |
38 | private extension Date {
39 | var yyyymmdd: String {
40 | let formatter = DateFormatter()
41 | formatter.dateFormat = "YYYYMMDD"
42 | return formatter.string(from: self)
43 | }
44 | var hhmm: String {
45 | let formatter = DateFormatter()
46 | formatter.dateFormat = "HH:mm"
47 | return formatter.string(from: self)
48 | }
49 | }
50 |
51 |
52 |
--------------------------------------------------------------------------------
/sui_sample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | NSLocationWhenInUseUsageDescription
29 | Get your location for weather forecast
30 | UIApplicationSceneManifest
31 |
32 | UIApplicationSupportsMultipleScenes
33 |
34 |
35 | UILaunchStoryboardName
36 | LaunchScreen
37 | UIRequiredDeviceCapabilities
38 |
39 | armv7
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UISupportedInterfaceOrientations~ipad
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationPortraitUpsideDown
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/sui_sample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/sui_sample/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/06.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct RootView: View {
13 | // stores
14 | @State var pageStore = PageStore()
15 | // actions
16 | @State var fetchCurrentLocation = FetchCurrentLocation()
17 | @State var readCitiesFromFile = ReadCitiesFromFile()
18 | @State var filterCities = FilterCities()
19 | @State var fetch5DaysForecast = Fetch5DaysForecast()
20 |
21 | var body: some View {
22 | NavigationStack(path: $pageStore.pages) {
23 | CitySelectionView()
24 | .navigationDestination(for: Page.self) { page in
25 | switch page {
26 | case .cityList: CitySelectionView()
27 | case .forecast(let searchType):
28 | ForecastView(searchType: searchType)
29 | }
30 | }
31 | }
32 | .environment(pageStore)
33 | .environment(\.fetchCurrentLocation, fetchCurrentLocation)
34 | .environment(\.readCitiesFromFile, readCitiesFromFile)
35 | .environment(\.filterCities, filterCities)
36 | .environment(\.fetch5DaysForecast, fetch5DaysForecast)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sui_sample/Stores/PageStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageStore.swift
3 | // sui_sample
4 | //
5 | // Created by nao on 2025/05/06.
6 | // Copyright © 2025 naodroid. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | enum Page: Sendable, Hashable {
12 | case cityList
13 | case forecast(searchType: WeatherSearchType)
14 | }
15 |
16 | @MainActor
17 | @Observable
18 | final class PageStore {
19 | var pages: [Page] = []
20 |
21 | func push(_ page: Page) {
22 | self.pages.append(page)
23 | }
24 | func pop() {
25 | self.pages.removeLast()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/sui_sample/Utils/Async+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Async+Ext.swift
3 | // sui_sample
4 | //
5 | // Created by 坂本尚嗣 on 2021/07/17.
6 | // Copyright © 2021 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Task {
12 | /// Currently Task.sleep(_:) causes crashes, so created own method to avoid this issue.
13 | /// https://bugs.swift.org/browse/SR-14375
14 | static func sleep(forSeconds seconds: TimeInterval) async {
15 | await AsyncSleeper().sleep(forSeconds: seconds)
16 | }
17 | }
18 |
19 |
20 | private struct AsyncSleeper {
21 | func sleep(forSeconds seconds: TimeInterval) async {
22 | typealias SleepContinuation = CheckedContinuation
23 | do {
24 | try await withCheckedThrowingContinuation { (continuation: SleepContinuation) in
25 | DispatchQueue.global().asyncAfter(deadline: .now() + seconds) {
26 | continuation.resume(with: .success(()))
27 | }
28 | }
29 | } catch {
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sui_sample/Utils/ResourceFunctions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResourceFunctions.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/07.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | enum ResoruceError : Error {
13 | case invalidURL
14 | case jsonParseError(Error)
15 | case connectionError(Error)
16 | case unknownError
17 | }
18 |
19 | /// Mainily for debug usage,
20 | /// use local resource instead of network access
21 | /// to avoid API limit.
22 | func resourceRequest(fileName: String) async throws -> Data {
23 | guard let path = Bundle.main.path(forResource: fileName, ofType: nil) else {
24 | throw ResoruceError.invalidURL
25 | }
26 | await Task.sleep(forSeconds: 1.0)
27 | let url = URL(fileURLWithPath: path)
28 | guard let data = try? Data(contentsOf: url) else {
29 | throw ResoruceError.invalidURL
30 | }
31 | return data
32 | }
33 |
34 | func resourceRequestJson(fileName: String) async throws -> T {
35 | let data = try await resourceRequest(fileName: fileName)
36 | let decoder = JSONDecoder()
37 | decoder.keyDecodingStrategy = .convertFromSnakeCase
38 | return try! JSONDecoder().decode(T.self, from: data)
39 | }
40 |
--------------------------------------------------------------------------------
/sui_sample/Utils/SwiftUI+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUI+Ext.swift
3 | // sui_sample
4 | //
5 | // Created by nadroid on 2019/06/05.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | extension View {
13 | func maximumWidth() -> some View {
14 | self.frame(minWidth: 0, idealWidth: 12000, maxWidth: 12000)
15 | }
16 |
17 | func maximumHeight() -> some View {
18 | self.frame(minHeight: 0, idealHeight: 12000, maxHeight: 12000)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sui_sample/View/CityListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CityListView.swift
3 | // sui_sample
4 | //
5 | // Created by nao on 2025/05/06.
6 | // Copyright © 2025 naodroid. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct CityListView: View {
12 | // dependescies
13 | @Environment(\.colorTheme) var colorTheme
14 | @Environment(\.readCitiesFromFile) var readCitiesFromFile
15 | @Environment(\.filterCities) var filterCities
16 |
17 | // states
18 | /// cities not filtered
19 | @State private var allCities: [City] = []
20 | /// filtered city
21 | @State private var cities: [City] = []
22 | /// search text
23 | @State private var text = ""
24 | /// store tasks for cancelling
25 | @State private var lastSearchTask: Task?
26 |
27 | /// body
28 | var body: some View {
29 | _body
30 | .onAppear {
31 | setup()
32 | }
33 | .onChange(of: text) { _, _ in
34 | filter()
35 | }
36 | }
37 | }
38 | // MARK: views
39 | private extension CityListView {
40 | @ViewBuilder
41 | private var _body: some View {
42 | VStack(spacing: 0) {
43 | textField
44 | List(
45 | cities,
46 | rowContent: { city in
47 | CityRow(city: city)
48 | }
49 | )
50 | }
51 | }
52 | @ViewBuilder
53 | private var textField: some View {
54 | TextField(
55 | text: $text,
56 | label: {
57 | Text("City Name")
58 | }
59 | )
60 | .padding(.horizontal, 8)
61 | .background(colorTheme.background)
62 | .cornerRadius(4.0)
63 | .padding(.vertical, 8)
64 | .padding(.horizontal, 16)
65 | .background(colorTheme.tabBgColor)
66 | }
67 | }
68 | // MARK: actions
69 | private extension CityListView {
70 | func setup() {
71 | Task {
72 | do {
73 | allCities = try await readCitiesFromFile()
74 | filter()
75 | } catch {
76 | print(error)
77 | }
78 | }
79 | }
80 | func filter() {
81 | lastSearchTask?.cancel()
82 | lastSearchTask = Task {
83 | do {
84 | // throttle
85 | try await Task.sleep(nanoseconds: 300 * 1000 * 1000)
86 | self.cities = await filterCities(
87 | keyword: text,
88 | allCities: allCities
89 | )
90 | } catch {
91 | print(error)
92 | }
93 | }
94 | }
95 | }
96 |
97 | // MARK: List item
98 | private struct CityRow: View {
99 | // dependencies
100 | @Environment(PageStore.self) var pageStore
101 | // params
102 | let city: City
103 | // body
104 | var body: some View {
105 | HStack {
106 | Text(city.name + "(" + city.country + ")")
107 | .lineLimit(2)
108 | .layoutPriority(1)
109 | Spacer()
110 | .layoutPriority(0)
111 | }
112 | .contentShape(Rectangle())
113 | .onTapGesture {
114 | let searchType: WeatherSearchType = .city(city)
115 | pageStore.push(
116 | .forecast(searchType: searchType)
117 | )
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/sui_sample/View/CitySelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CitySelectView.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/06.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 | import CoreLocationUI
11 | import Foundation
12 | import SwiftUI
13 |
14 |
15 | struct CitySelectionView: View {
16 | // dependencies
17 | @Environment(\.colorTheme) var colorTheme
18 | // states
19 | @State var selectedTab: Int = 0
20 | @State var cities: [City] = []
21 | @State var locating = false
22 | @State var location: CLLocation? = nil
23 | @State var pin: CLLocationCoordinate2D? = nil
24 | @State var text: String = ""
25 |
26 | private var lat: Double { pin?.latitude ?? 0 }
27 | private var lon: Double { pin?.longitude ?? 0 }
28 |
29 | //------------------------
30 | var body: some View {
31 | TabView {
32 | CityListView()
33 | .tag(0)
34 | .toolbarBackground(.visible, for: .tabBar)
35 | .tabItem {
36 | Label("List", systemImage: "list.bullet")
37 | }
38 | LocationSelectionView()
39 | .tag(1)
40 | .toolbarBackground(.visible, for: .tabBar)
41 | .tabItem {
42 | Label("Map", systemImage: "mappin.and.ellipse")
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/sui_sample/View/ForecastView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Weather5DayView.swift
3 | // sui_sample
4 | //
5 | // Created by naodroid on 2019/06/07.
6 | // Copyright © 2019 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import WeatherKit
12 |
13 | enum WeatherSearchType: Hashable, Sendable {
14 | case city(City)
15 | case location(lat: Double, lon: Double)
16 | }
17 |
18 | struct ForecastView: View {
19 | // dependescies
20 | @Environment(\.colorTheme) var colorTheme
21 | @Environment(\.fetch5DaysForecast) var fetch5DaysForecast
22 | // params
23 | let searchType: WeatherSearchType
24 | // states
25 | @State private var forecasts: [DayForecast] = []
26 | private var hasForecast: Bool { !forecasts.isEmpty }
27 |
28 | var navigationTitle: String {
29 | switch searchType {
30 | case .city(let city): return city.name
31 | case .location(let lat, let lon):
32 | return String.init(format: "%.3f,%.3f", lat, lon)
33 | }
34 | }
35 |
36 | var body: some View {
37 | _body
38 | .navigationBarTitle(
39 | Text(self.navigationTitle),
40 | displayMode: .large
41 | )
42 | .onAppear {
43 | fetch()
44 | }
45 | }
46 | }
47 | // MARK: views
48 | extension ForecastView {
49 | @ViewBuilder
50 | fileprivate var _body: some View {
51 | if forecasts.isEmpty {
52 | Text("Loading")
53 | } else {
54 | List(
55 | forecasts,
56 | rowContent: { (item) in
57 | ForecastRow(item: item)
58 | })
59 | }
60 | }
61 | }
62 | // MARK: actions
63 | extension ForecastView {
64 | private func fetch() {
65 | Task {
66 | do {
67 | switch searchType {
68 | case .city(let city):
69 | forecasts = try await fetch5DaysForecast(city: city)
70 | case .location(let lat, let lon):
71 | forecasts = try await fetch5DaysForecast(lat: lat, lon: lon)
72 |
73 | }
74 | } catch {
75 | print(error)
76 | }
77 | }
78 | }
79 | }
80 | // MARK: list item
81 | private struct ForecastRow: View {
82 | @Environment(\.colorTheme) var colorTheme
83 | // params
84 | let item: DayForecast
85 |
86 | var list: [HourForecast] {
87 | return item.hourly
88 | }
89 |
90 | var body: some View {
91 | VStack(alignment: .leading, spacing: 4) {
92 | Text(item.forecast.date.formatted())
93 | .padding(.horizontal, 8)
94 | ScrollView(
95 | Axis.Set.horizontal,
96 | showsIndicators: false
97 | ) {
98 | HStack {
99 | ForEach(list, id: \.id) { (item) in
100 | VStack {
101 | Image(systemName: item.forecast.symbolName)
102 | Text(item.hhmm)
103 | .font(.footnote)
104 | }
105 | .frame(width: CGFloat(60))
106 | }
107 | }
108 | }
109 | }
110 | .padding(.all, 4)
111 | .frame(height: 120)
112 | .background(colorTheme.background)
113 | .shadow(radius: 3)
114 | .padding(.all, 4)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/sui_sample/View/LocationSelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationSelectionView.swift
3 | // sui_sample
4 | //
5 | // Created by nao on 2025/05/06.
6 | // Copyright © 2025 naodroid. All rights reserved.
7 | //
8 |
9 | @preconcurrency import CoreLocation
10 | import CoreLocationUI
11 | import Foundation
12 | import SwiftUI
13 |
14 | struct LocationSelectionView: View {
15 | // dependencies
16 | @Environment(PageStore.self) var pageStore
17 | @Environment(\.colorTheme) var colorTheme
18 | @Environment(\.fetchCurrentLocation) var fetchCurrentLocation
19 | // states
20 | @State private var location: CLLocation?
21 | @State private var pin: CLLocationCoordinate2D? = nil
22 | @State private var locating = false
23 | // body
24 | var body: some View {
25 | _body
26 | }
27 | }
28 |
29 | // MARK: views
30 | extension LocationSelectionView {
31 | @ViewBuilder
32 | private var _body: some View {
33 | ZStack {
34 | MapView(
35 | loc: location,
36 | pin: pin,
37 | onTap: { (pos) in
38 | pin = pos
39 | }
40 | )
41 | VStack {
42 | Text("Tap to select place")
43 | Spacer().layoutPriority(1)
44 | locationButton
45 | nextButton
46 | Spacer().frame(height: 20)
47 | }
48 | progress
49 | }
50 | }
51 | @ViewBuilder
52 | private var locationButton: some View {
53 | HStack {
54 | Spacer()
55 | LocationButton(.currentLocation) {
56 | fetchLocation()
57 | }.labelStyle(.titleOnly)
58 | .foregroundColor(Color.white)
59 | .symbolVariant(.fill)
60 | .cornerRadius(8)
61 | .disabled(locating)
62 | Spacer().frame(width: 8)
63 | }
64 | }
65 | @ViewBuilder
66 | private var nextButton: some View {
67 | Button {
68 | if let location {
69 | let searchType = WeatherSearchType.location(
70 | lat: location.coordinate.latitude,
71 | lon: location.coordinate.longitude
72 | )
73 | pageStore.push(
74 | .forecast(searchType: searchType)
75 | )
76 | }
77 | } label: {
78 | Text("Use Center")
79 | .foregroundColor(Color.white)
80 | .padding(.horizontal, 16)
81 | .padding(.vertical, 8)
82 | .background(Color.blue)
83 | .clipShape(RoundedRectangle(cornerRadius: 8))
84 | .disabled(pin == nil)
85 | }
86 | }
87 |
88 | @ViewBuilder
89 | private var progress: some View {
90 | if locating {
91 | ProgressView("Locating...")
92 | .progressViewStyle(
93 | CircularProgressViewStyle(tint: Color.white)
94 | )
95 | .foregroundColor(Color.white)
96 | .frame(
97 | width: 120,
98 | height: 120,
99 | alignment: .center
100 | )
101 | .background(Color.black.opacity(0.5))
102 | .cornerRadius(16)
103 | }
104 | }
105 | }
106 | // MARK: actions
107 | extension LocationSelectionView {
108 | private func fetchLocation() {
109 | Task {
110 | locating = true
111 | defer {
112 | locating = false
113 | }
114 | do {
115 | location = try await fetchCurrentLocation()
116 | } catch {
117 | }
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/sui_sample/WeatherApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WeatherApp.swift
3 | // WeatherApp
4 | //
5 | // Created by nao on 2021/07/23.
6 | // Copyright © 2021 naodroid. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | @main
13 | struct WeatherApp: App {
14 | var body: some Scene {
15 | let theme = ColorTheme()
16 | let contentView = RootView()
17 | .environment(\.colorTheme, theme)
18 | let root = TraitObservingViewController(rootView: contentView)
19 | root.theme = theme
20 | return WindowGroup {
21 | contentView
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sui_sample/asset/5day_sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "cod": "200",
3 | "message": 0.0032,
4 | "cnt": 36,
5 | "list": [
6 | {
7 | "dt": 1487246400,
8 | "main": {
9 | "temp": 286.67,
10 | "temp_min": 281.556,
11 | "temp_max": 286.67,
12 | "pressure": 972.73,
13 | "sea_level": 1046.46,
14 | "grnd_level": 972.73,
15 | "humidity": 75,
16 | "temp_kf": 5.11
17 | },
18 | "weather": [
19 | {
20 | "id": 800,
21 | "main": "Clear",
22 | "description": "clear sky",
23 | "icon": "01d"
24 | }
25 | ],
26 | "clouds": {
27 | "all": 0
28 | },
29 | "wind": {
30 | "speed": 1.81,
31 | "deg": 247.501
32 | },
33 | "sys": {
34 | "pod": "d"
35 | },
36 | "dt_txt": "2017-02-16 12:00:00"
37 | },
38 | {
39 | "dt": 1487257200,
40 | "main": {
41 | "temp": 285.66,
42 | "temp_min": 281.821,
43 | "temp_max": 285.66,
44 | "pressure": 970.91,
45 | "sea_level": 1044.32,
46 | "grnd_level": 970.91,
47 | "humidity": 70,
48 | "temp_kf": 3.84
49 | },
50 | "weather": [
51 | {
52 | "id": 800,
53 | "main": "Clear",
54 | "description": "clear sky",
55 | "icon": "01d"
56 | }
57 | ],
58 | "clouds": {
59 | "all": 0
60 | },
61 | "wind": {
62 | "speed": 1.59,
63 | "deg": 290.501
64 | },
65 | "sys": {
66 | "pod": "d"
67 | },
68 | "dt_txt": "2017-02-16 15:00:00"
69 | },
70 | {
71 | "dt": 1487268000,
72 | "main": {
73 | "temp": 277.05,
74 | "temp_min": 274.498,
75 | "temp_max": 277.05,
76 | "pressure": 970.44,
77 | "sea_level": 1044.7,
78 | "grnd_level": 970.44,
79 | "humidity": 90,
80 | "temp_kf": 2.56
81 | },
82 | "weather": [
83 | {
84 | "id": 800,
85 | "main": "Clear",
86 | "description": "clear sky",
87 | "icon": "01n"
88 | }
89 | ],
90 | "clouds": {
91 | "all": 0
92 | },
93 | "wind": {
94 | "speed": 1.41,
95 | "deg": 263.5
96 | },
97 | "sys": {
98 | "pod": "n"
99 | },
100 | "dt_txt": "2017-02-16 18:00:00"
101 | },
102 | {
103 | "dt": 1487278800,
104 | "main": {
105 | "temp": 272.78,
106 | "temp_min": 271.503,
107 | "temp_max": 272.78,
108 | "pressure": 969.32,
109 | "sea_level": 1044.14,
110 | "grnd_level": 969.32,
111 | "humidity": 80,
112 | "temp_kf": 1.28
113 | },
114 | "weather": [
115 | {
116 | "id": 800,
117 | "main": "Clear",
118 | "description": "clear sky",
119 | "icon": "01n"
120 | }
121 | ],
122 | "clouds": {
123 | "all": 0
124 | },
125 | "wind": {
126 | "speed": 2.24,
127 | "deg": 205.502
128 | },
129 | "sys": {
130 | "pod": "n"
131 | },
132 | "dt_txt": "2017-02-16 21:00:00"
133 | },
134 | {
135 | "dt": 1487289600,
136 | "main": {
137 | "temp": 273.341,
138 | "temp_min": 273.341,
139 | "temp_max": 273.341,
140 | "pressure": 968.14,
141 | "sea_level": 1042.96,
142 | "grnd_level": 968.14,
143 | "humidity": 85,
144 | "temp_kf": 0
145 | },
146 | "weather": [
147 | {
148 | "id": 803,
149 | "main": "Clouds",
150 | "description": "broken clouds",
151 | "icon": "04n"
152 | }
153 | ],
154 | "clouds": {
155 | "all": 76
156 | },
157 | "wind": {
158 | "speed": 3.59,
159 | "deg": 224.003
160 | },
161 | "sys": {
162 | "pod": "n"
163 | },
164 | "dt_txt": "2017-02-17 00:00:00"
165 | },
166 | {
167 | "dt": 1487300400,
168 | "main": {
169 | "temp": 275.568,
170 | "temp_min": 275.568,
171 | "temp_max": 275.568,
172 | "pressure": 966.6,
173 | "sea_level": 1041.39,
174 | "grnd_level": 966.6,
175 | "humidity": 89,
176 | "temp_kf": 0
177 | },
178 | "weather": [
179 | {
180 | "id": 500,
181 | "main": "Rain",
182 | "description": "light rain",
183 | "icon": "10n"
184 | }
185 | ],
186 | "clouds": {
187 | "all": 76
188 | },
189 | "wind": {
190 | "speed": 3.77,
191 | "deg": 237.002
192 | },
193 | "rain": {
194 | "3h": 0.32
195 | },
196 | "sys": {
197 | "pod": "n"
198 | },
199 | "dt_txt": "2017-02-17 03:00:00"
200 | },
201 | {
202 | "dt": 1487311200,
203 | "main": {
204 | "temp": 276.478,
205 | "temp_min": 276.478,
206 | "temp_max": 276.478,
207 | "pressure": 966.45,
208 | "sea_level": 1041.21,
209 | "grnd_level": 966.45,
210 | "humidity": 97,
211 | "temp_kf": 0
212 | },
213 | "weather": [
214 | {
215 | "id": 501,
216 | "main": "Rain",
217 | "description": "moderate rain",
218 | "icon": "10n"
219 | }
220 | ],
221 | "clouds": {
222 | "all": 92
223 | },
224 | "wind": {
225 | "speed": 3.81,
226 | "deg": 268.005
227 | },
228 | "rain": {
229 | "3h": 4.9
230 | },
231 | "sys": {
232 | "pod": "n"
233 | },
234 | "dt_txt": "2017-02-17 06:00:00"
235 | },
236 | {
237 | "dt": 1487322000,
238 | "main": {
239 | "temp": 276.67,
240 | "temp_min": 276.67,
241 | "temp_max": 276.67,
242 | "pressure": 967.41,
243 | "sea_level": 1041.95,
244 | "grnd_level": 967.41,
245 | "humidity": 100,
246 | "temp_kf": 0
247 | },
248 | "weather": [
249 | {
250 | "id": 500,
251 | "main": "Rain",
252 | "description": "light rain",
253 | "icon": "10d"
254 | }
255 | ],
256 | "clouds": {
257 | "all": 64
258 | },
259 | "wind": {
260 | "speed": 2.6,
261 | "deg": 266.504
262 | },
263 | "rain": {
264 | "3h": 1.37
265 | },
266 | "sys": {
267 | "pod": "d"
268 | },
269 | "dt_txt": "2017-02-17 09:00:00"
270 | },
271 | {
272 | "dt": 1487332800,
273 | "main": {
274 | "temp": 278.253,
275 | "temp_min": 278.253,
276 | "temp_max": 278.253,
277 | "pressure": 966.98,
278 | "sea_level": 1040.89,
279 | "grnd_level": 966.98,
280 | "humidity": 95,
281 | "temp_kf": 0
282 | },
283 | "weather": [
284 | {
285 | "id": 500,
286 | "main": "Rain",
287 | "description": "light rain",
288 | "icon": "10d"
289 | }
290 | ],
291 | "clouds": {
292 | "all": 92
293 | },
294 | "wind": {
295 | "speed": 3.17,
296 | "deg": 261.501
297 | },
298 | "rain": {
299 | "3h": 0.12
300 | },
301 | "sys": {
302 | "pod": "d"
303 | },
304 | "dt_txt": "2017-02-17 12:00:00"
305 | },
306 | {
307 | "dt": 1487343600,
308 | "main": {
309 | "temp": 276.455,
310 | "temp_min": 276.455,
311 | "temp_max": 276.455,
312 | "pressure": 966.38,
313 | "sea_level": 1040.17,
314 | "grnd_level": 966.38,
315 | "humidity": 99,
316 | "temp_kf": 0
317 | },
318 | "weather": [
319 | {
320 | "id": 500,
321 | "main": "Rain",
322 | "description": "light rain",
323 | "icon": "10d"
324 | }
325 | ],
326 | "clouds": {
327 | "all": 92
328 | },
329 | "wind": {
330 | "speed": 3.21,
331 | "deg": 268.001
332 | },
333 | "rain": {
334 | "3h": 2.12
335 | },
336 | "sys": {
337 | "pod": "d"
338 | },
339 | "dt_txt": "2017-02-17 15:00:00"
340 | },
341 | {
342 | "dt": 1487354400,
343 | "main": {
344 | "temp": 275.639,
345 | "temp_min": 275.639,
346 | "temp_max": 275.639,
347 | "pressure": 966.39,
348 | "sea_level": 1040.65,
349 | "grnd_level": 966.39,
350 | "humidity": 95,
351 | "temp_kf": 0
352 | },
353 | "weather": [
354 | {
355 | "id": 500,
356 | "main": "Rain",
357 | "description": "light rain",
358 | "icon": "10n"
359 | }
360 | ],
361 | "clouds": {
362 | "all": 88
363 | },
364 | "wind": {
365 | "speed": 3.17,
366 | "deg": 258.001
367 | },
368 | "rain": {
369 | "3h": 0.7
370 | },
371 | "snow": {
372 | "3h": 0.0775
373 | },
374 | "sys": {
375 | "pod": "n"
376 | },
377 | "dt_txt": "2017-02-17 18:00:00"
378 | },
379 | {
380 | "dt": 1487365200,
381 | "main": {
382 | "temp": 275.459,
383 | "temp_min": 275.459,
384 | "temp_max": 275.459,
385 | "pressure": 966.3,
386 | "sea_level": 1040.8,
387 | "grnd_level": 966.3,
388 | "humidity": 96,
389 | "temp_kf": 0
390 | },
391 | "weather": [
392 | {
393 | "id": 500,
394 | "main": "Rain",
395 | "description": "light rain",
396 | "icon": "10n"
397 | }
398 | ],
399 | "clouds": {
400 | "all": 88
401 | },
402 | "wind": {
403 | "speed": 3.71,
404 | "deg": 265.503
405 | },
406 | "rain": {
407 | "3h": 1.16
408 | },
409 | "snow": {
410 | "3h": 0.075
411 | },
412 | "sys": {
413 | "pod": "n"
414 | },
415 | "dt_txt": "2017-02-17 21:00:00"
416 | },
417 | {
418 | "dt": 1487376000,
419 | "main": {
420 | "temp": 275.035,
421 | "temp_min": 275.035,
422 | "temp_max": 275.035,
423 | "pressure": 966.43,
424 | "sea_level": 1041.02,
425 | "grnd_level": 966.43,
426 | "humidity": 99,
427 | "temp_kf": 0
428 | },
429 | "weather": [
430 | {
431 | "id": 500,
432 | "main": "Rain",
433 | "description": "light rain",
434 | "icon": "10n"
435 | }
436 | ],
437 | "clouds": {
438 | "all": 92
439 | },
440 | "wind": {
441 | "speed": 3.56,
442 | "deg": 273.5
443 | },
444 | "rain": {
445 | "3h": 1.37
446 | },
447 | "snow": {
448 | "3h": 0.1525
449 | },
450 | "sys": {
451 | "pod": "n"
452 | },
453 | "dt_txt": "2017-02-18 00:00:00"
454 | },
455 | {
456 | "dt": 1487386800,
457 | "main": {
458 | "temp": 274.965,
459 | "temp_min": 274.965,
460 | "temp_max": 274.965,
461 | "pressure": 966.36,
462 | "sea_level": 1041.17,
463 | "grnd_level": 966.36,
464 | "humidity": 97,
465 | "temp_kf": 0
466 | },
467 | "weather": [
468 | {
469 | "id": 500,
470 | "main": "Rain",
471 | "description": "light rain",
472 | "icon": "10n"
473 | }
474 | ],
475 | "clouds": {
476 | "all": 88
477 | },
478 | "wind": {
479 | "speed": 2.66,
480 | "deg": 285.502
481 | },
482 | "rain": {
483 | "3h": 0.79
484 | },
485 | "snow": {
486 | "3h": 0.52
487 | },
488 | "sys": {
489 | "pod": "n"
490 | },
491 | "dt_txt": "2017-02-18 03:00:00"
492 | },
493 | {
494 | "dt": 1487397600,
495 | "main": {
496 | "temp": 274.562,
497 | "temp_min": 274.562,
498 | "temp_max": 274.562,
499 | "pressure": 966.75,
500 | "sea_level": 1041.57,
501 | "grnd_level": 966.75,
502 | "humidity": 98,
503 | "temp_kf": 0
504 | },
505 | "weather": [
506 | {
507 | "id": 500,
508 | "main": "Rain",
509 | "description": "light rain",
510 | "icon": "10n"
511 | }
512 | ],
513 | "clouds": {
514 | "all": 88
515 | },
516 | "wind": {
517 | "speed": 1.46,
518 | "deg": 276.5
519 | },
520 | "rain": {
521 | "3h": 0.08
522 | },
523 | "snow": {
524 | "3h": 0.06
525 | },
526 | "sys": {
527 | "pod": "n"
528 | },
529 | "dt_txt": "2017-02-18 06:00:00"
530 | },
531 | {
532 | "dt": 1487408400,
533 | "main": {
534 | "temp": 275.648,
535 | "temp_min": 275.648,
536 | "temp_max": 275.648,
537 | "pressure": 967.21,
538 | "sea_level": 1041.74,
539 | "grnd_level": 967.21,
540 | "humidity": 99,
541 | "temp_kf": 0
542 | },
543 | "weather": [
544 | {
545 | "id": 500,
546 | "main": "Rain",
547 | "description": "light rain",
548 | "icon": "10d"
549 | }
550 | ],
551 | "clouds": {
552 | "all": 56
553 | },
554 | "wind": {
555 | "speed": 1.5,
556 | "deg": 251.008
557 | },
558 | "rain": {
559 | "3h": 0.02
560 | },
561 | "snow": {
562 | "3h": 0.03
563 | },
564 | "sys": {
565 | "pod": "d"
566 | },
567 | "dt_txt": "2017-02-18 09:00:00"
568 | },
569 | {
570 | "dt": 1487419200,
571 | "main": {
572 | "temp": 277.927,
573 | "temp_min": 277.927,
574 | "temp_max": 277.927,
575 | "pressure": 966.06,
576 | "sea_level": 1039.98,
577 | "grnd_level": 966.06,
578 | "humidity": 95,
579 | "temp_kf": 0
580 | },
581 | "weather": [
582 | {
583 | "id": 800,
584 | "main": "Clear",
585 | "description": "clear sky",
586 | "icon": "02d"
587 | }
588 | ],
589 | "clouds": {
590 | "all": 8
591 | },
592 | "wind": {
593 | "speed": 0.86,
594 | "deg": 244.004
595 | },
596 | "rain": {},
597 | "snow": {},
598 | "sys": {
599 | "pod": "d"
600 | },
601 | "dt_txt": "2017-02-18 12:00:00"
602 | },
603 | {
604 | "dt": 1487430000,
605 | "main": {
606 | "temp": 278.367,
607 | "temp_min": 278.367,
608 | "temp_max": 278.367,
609 | "pressure": 964.57,
610 | "sea_level": 1038.35,
611 | "grnd_level": 964.57,
612 | "humidity": 89,
613 | "temp_kf": 0
614 | },
615 | "weather": [
616 | {
617 | "id": 800,
618 | "main": "Clear",
619 | "description": "clear sky",
620 | "icon": "02d"
621 | }
622 | ],
623 | "clouds": {
624 | "all": 8
625 | },
626 | "wind": {
627 | "speed": 1.62,
628 | "deg": 79.5024
629 | },
630 | "rain": {},
631 | "snow": {},
632 | "sys": {
633 | "pod": "d"
634 | },
635 | "dt_txt": "2017-02-18 15:00:00"
636 | },
637 | {
638 | "dt": 1487440800,
639 | "main": {
640 | "temp": 273.797,
641 | "temp_min": 273.797,
642 | "temp_max": 273.797,
643 | "pressure": 964.13,
644 | "sea_level": 1038.48,
645 | "grnd_level": 964.13,
646 | "humidity": 91,
647 | "temp_kf": 0
648 | },
649 | "weather": [
650 | {
651 | "id": 800,
652 | "main": "Clear",
653 | "description": "clear sky",
654 | "icon": "01n"
655 | }
656 | ],
657 | "clouds": {
658 | "all": 0
659 | },
660 | "wind": {
661 | "speed": 2.42,
662 | "deg": 77.0026
663 | },
664 | "rain": {},
665 | "snow": {},
666 | "sys": {
667 | "pod": "n"
668 | },
669 | "dt_txt": "2017-02-18 18:00:00"
670 | },
671 | {
672 | "dt": 1487451600,
673 | "main": {
674 | "temp": 271.239,
675 | "temp_min": 271.239,
676 | "temp_max": 271.239,
677 | "pressure": 963.39,
678 | "sea_level": 1038.21,
679 | "grnd_level": 963.39,
680 | "humidity": 93,
681 | "temp_kf": 0
682 | },
683 | "weather": [
684 | {
685 | "id": 800,
686 | "main": "Clear",
687 | "description": "clear sky",
688 | "icon": "01n"
689 | }
690 | ],
691 | "clouds": {
692 | "all": 0
693 | },
694 | "wind": {
695 | "speed": 2.42,
696 | "deg": 95.5017
697 | },
698 | "rain": {},
699 | "snow": {},
700 | "sys": {
701 | "pod": "n"
702 | },
703 | "dt_txt": "2017-02-18 21:00:00"
704 | },
705 | {
706 | "dt": 1487462400,
707 | "main": {
708 | "temp": 269.553,
709 | "temp_min": 269.553,
710 | "temp_max": 269.553,
711 | "pressure": 962.39,
712 | "sea_level": 1037.44,
713 | "grnd_level": 962.39,
714 | "humidity": 92,
715 | "temp_kf": 0
716 | },
717 | "weather": [
718 | {
719 | "id": 800,
720 | "main": "Clear",
721 | "description": "clear sky",
722 | "icon": "01n"
723 | }
724 | ],
725 | "clouds": {
726 | "all": 0
727 | },
728 | "wind": {
729 | "speed": 1.96,
730 | "deg": 101.004
731 | },
732 | "rain": {},
733 | "snow": {},
734 | "sys": {
735 | "pod": "n"
736 | },
737 | "dt_txt": "2017-02-19 00:00:00"
738 | },
739 | {
740 | "dt": 1487473200,
741 | "main": {
742 | "temp": 268.198,
743 | "temp_min": 268.198,
744 | "temp_max": 268.198,
745 | "pressure": 961.28,
746 | "sea_level": 1036.51,
747 | "grnd_level": 961.28,
748 | "humidity": 84,
749 | "temp_kf": 0
750 | },
751 | "weather": [
752 | {
753 | "id": 800,
754 | "main": "Clear",
755 | "description": "clear sky",
756 | "icon": "01n"
757 | }
758 | ],
759 | "clouds": {
760 | "all": 0
761 | },
762 | "wind": {
763 | "speed": 1.06,
764 | "deg": 121.5
765 | },
766 | "rain": {},
767 | "snow": {},
768 | "sys": {
769 | "pod": "n"
770 | },
771 | "dt_txt": "2017-02-19 03:00:00"
772 | },
773 | {
774 | "dt": 1487484000,
775 | "main": {
776 | "temp": 267.295,
777 | "temp_min": 267.295,
778 | "temp_max": 267.295,
779 | "pressure": 961.16,
780 | "sea_level": 1036.45,
781 | "grnd_level": 961.16,
782 | "humidity": 86,
783 | "temp_kf": 0
784 | },
785 | "weather": [
786 | {
787 | "id": 800,
788 | "main": "Clear",
789 | "description": "clear sky",
790 | "icon": "01n"
791 | }
792 | ],
793 | "clouds": {
794 | "all": 0
795 | },
796 | "wind": {
797 | "speed": 1.17,
798 | "deg": 155.005
799 | },
800 | "rain": {},
801 | "snow": {},
802 | "sys": {
803 | "pod": "n"
804 | },
805 | "dt_txt": "2017-02-19 06:00:00"
806 | },
807 | {
808 | "dt": 1487494800,
809 | "main": {
810 | "temp": 272.956,
811 | "temp_min": 272.956,
812 | "temp_max": 272.956,
813 | "pressure": 962.03,
814 | "sea_level": 1036.85,
815 | "grnd_level": 962.03,
816 | "humidity": 84,
817 | "temp_kf": 0
818 | },
819 | "weather": [
820 | {
821 | "id": 800,
822 | "main": "Clear",
823 | "description": "clear sky",
824 | "icon": "01d"
825 | }
826 | ],
827 | "clouds": {
828 | "all": 0
829 | },
830 | "wind": {
831 | "speed": 1.66,
832 | "deg": 195.002
833 | },
834 | "rain": {},
835 | "snow": {},
836 | "sys": {
837 | "pod": "d"
838 | },
839 | "dt_txt": "2017-02-19 09:00:00"
840 | },
841 | {
842 | "dt": 1487505600,
843 | "main": {
844 | "temp": 277.422,
845 | "temp_min": 277.422,
846 | "temp_max": 277.422,
847 | "pressure": 962.23,
848 | "sea_level": 1036.06,
849 | "grnd_level": 962.23,
850 | "humidity": 89,
851 | "temp_kf": 0
852 | },
853 | "weather": [
854 | {
855 | "id": 800,
856 | "main": "Clear",
857 | "description": "clear sky",
858 | "icon": "01d"
859 | }
860 | ],
861 | "clouds": {
862 | "all": 0
863 | },
864 | "wind": {
865 | "speed": 1.32,
866 | "deg": 357.003
867 | },
868 | "rain": {},
869 | "snow": {},
870 | "sys": {
871 | "pod": "d"
872 | },
873 | "dt_txt": "2017-02-19 12:00:00"
874 | },
875 | {
876 | "dt": 1487516400,
877 | "main": {
878 | "temp": 277.984,
879 | "temp_min": 277.984,
880 | "temp_max": 277.984,
881 | "pressure": 962.15,
882 | "sea_level": 1035.86,
883 | "grnd_level": 962.15,
884 | "humidity": 87,
885 | "temp_kf": 0
886 | },
887 | "weather": [
888 | {
889 | "id": 800,
890 | "main": "Clear",
891 | "description": "clear sky",
892 | "icon": "01d"
893 | }
894 | ],
895 | "clouds": {
896 | "all": 0
897 | },
898 | "wind": {
899 | "speed": 1.58,
900 | "deg": 48.5031
901 | },
902 | "rain": {},
903 | "snow": {},
904 | "sys": {
905 | "pod": "d"
906 | },
907 | "dt_txt": "2017-02-19 15:00:00"
908 | },
909 | {
910 | "dt": 1487527200,
911 | "main": {
912 | "temp": 272.459,
913 | "temp_min": 272.459,
914 | "temp_max": 272.459,
915 | "pressure": 963.31,
916 | "sea_level": 1037.81,
917 | "grnd_level": 963.31,
918 | "humidity": 90,
919 | "temp_kf": 0
920 | },
921 | "weather": [
922 | {
923 | "id": 800,
924 | "main": "Clear",
925 | "description": "clear sky",
926 | "icon": "01n"
927 | }
928 | ],
929 | "clouds": {
930 | "all": 0
931 | },
932 | "wind": {
933 | "speed": 1.16,
934 | "deg": 75.5042
935 | },
936 | "rain": {},
937 | "snow": {},
938 | "sys": {
939 | "pod": "n"
940 | },
941 | "dt_txt": "2017-02-19 18:00:00"
942 | },
943 | {
944 | "dt": 1487538000,
945 | "main": {
946 | "temp": 269.473,
947 | "temp_min": 269.473,
948 | "temp_max": 269.473,
949 | "pressure": 964.65,
950 | "sea_level": 1039.76,
951 | "grnd_level": 964.65,
952 | "humidity": 83,
953 | "temp_kf": 0
954 | },
955 | "weather": [
956 | {
957 | "id": 800,
958 | "main": "Clear",
959 | "description": "clear sky",
960 | "icon": "01n"
961 | }
962 | ],
963 | "clouds": {
964 | "all": 0
965 | },
966 | "wind": {
967 | "speed": 1.12,
968 | "deg": 174.002
969 | },
970 | "rain": {},
971 | "snow": {},
972 | "sys": {
973 | "pod": "n"
974 | },
975 | "dt_txt": "2017-02-19 21:00:00"
976 | },
977 | {
978 | "dt": 1487548800,
979 | "main": {
980 | "temp": 268.793,
981 | "temp_min": 268.793,
982 | "temp_max": 268.793,
983 | "pressure": 965.92,
984 | "sea_level": 1041.32,
985 | "grnd_level": 965.92,
986 | "humidity": 80,
987 | "temp_kf": 0
988 | },
989 | "weather": [
990 | {
991 | "id": 800,
992 | "main": "Clear",
993 | "description": "clear sky",
994 | "icon": "01n"
995 | }
996 | ],
997 | "clouds": {
998 | "all": 0
999 | },
1000 | "wind": {
1001 | "speed": 2.11,
1002 | "deg": 207.502
1003 | },
1004 | "rain": {},
1005 | "snow": {},
1006 | "sys": {
1007 | "pod": "n"
1008 | },
1009 | "dt_txt": "2017-02-20 00:00:00"
1010 | },
1011 | {
1012 | "dt": 1487559600,
1013 | "main": {
1014 | "temp": 268.106,
1015 | "temp_min": 268.106,
1016 | "temp_max": 268.106,
1017 | "pressure": 966.4,
1018 | "sea_level": 1042.18,
1019 | "grnd_level": 966.4,
1020 | "humidity": 85,
1021 | "temp_kf": 0
1022 | },
1023 | "weather": [
1024 | {
1025 | "id": 800,
1026 | "main": "Clear",
1027 | "description": "clear sky",
1028 | "icon": "01n"
1029 | }
1030 | ],
1031 | "clouds": {
1032 | "all": 0
1033 | },
1034 | "wind": {
1035 | "speed": 1.67,
1036 | "deg": 191.001
1037 | },
1038 | "rain": {},
1039 | "snow": {},
1040 | "sys": {
1041 | "pod": "n"
1042 | },
1043 | "dt_txt": "2017-02-20 03:00:00"
1044 | },
1045 | {
1046 | "dt": 1487570400,
1047 | "main": {
1048 | "temp": 267.655,
1049 | "temp_min": 267.655,
1050 | "temp_max": 267.655,
1051 | "pressure": 967.4,
1052 | "sea_level": 1043.43,
1053 | "grnd_level": 967.4,
1054 | "humidity": 84,
1055 | "temp_kf": 0
1056 | },
1057 | "weather": [
1058 | {
1059 | "id": 800,
1060 | "main": "Clear",
1061 | "description": "clear sky",
1062 | "icon": "01n"
1063 | }
1064 | ],
1065 | "clouds": {
1066 | "all": 0
1067 | },
1068 | "wind": {
1069 | "speed": 1.61,
1070 | "deg": 194.001
1071 | },
1072 | "rain": {},
1073 | "snow": {},
1074 | "sys": {
1075 | "pod": "n"
1076 | },
1077 | "dt_txt": "2017-02-20 06:00:00"
1078 | },
1079 | {
1080 | "dt": 1487581200,
1081 | "main": {
1082 | "temp": 273.75,
1083 | "temp_min": 273.75,
1084 | "temp_max": 273.75,
1085 | "pressure": 968.84,
1086 | "sea_level": 1044.23,
1087 | "grnd_level": 968.84,
1088 | "humidity": 83,
1089 | "temp_kf": 0
1090 | },
1091 | "weather": [
1092 | {
1093 | "id": 800,
1094 | "main": "Clear",
1095 | "description": "clear sky",
1096 | "icon": "01d"
1097 | }
1098 | ],
1099 | "clouds": {
1100 | "all": 0
1101 | },
1102 | "wind": {
1103 | "speed": 2.49,
1104 | "deg": 208.5
1105 | },
1106 | "rain": {},
1107 | "snow": {},
1108 | "sys": {
1109 | "pod": "d"
1110 | },
1111 | "dt_txt": "2017-02-20 09:00:00"
1112 | },
1113 | {
1114 | "dt": 1487592000,
1115 | "main": {
1116 | "temp": 279.302,
1117 | "temp_min": 279.302,
1118 | "temp_max": 279.302,
1119 | "pressure": 968.37,
1120 | "sea_level": 1042.52,
1121 | "grnd_level": 968.37,
1122 | "humidity": 83,
1123 | "temp_kf": 0
1124 | },
1125 | "weather": [
1126 | {
1127 | "id": 800,
1128 | "main": "Clear",
1129 | "description": "clear sky",
1130 | "icon": "01d"
1131 | }
1132 | ],
1133 | "clouds": {
1134 | "all": 0
1135 | },
1136 | "wind": {
1137 | "speed": 2.46,
1138 | "deg": 252.001
1139 | },
1140 | "rain": {},
1141 | "snow": {},
1142 | "sys": {
1143 | "pod": "d"
1144 | },
1145 | "dt_txt": "2017-02-20 12:00:00"
1146 | },
1147 | {
1148 | "dt": 1487602800,
1149 | "main": {
1150 | "temp": 279.343,
1151 | "temp_min": 279.343,
1152 | "temp_max": 279.343,
1153 | "pressure": 967.9,
1154 | "sea_level": 1041.64,
1155 | "grnd_level": 967.9,
1156 | "humidity": 81,
1157 | "temp_kf": 0
1158 | },
1159 | "weather": [
1160 | {
1161 | "id": 800,
1162 | "main": "Clear",
1163 | "description": "clear sky",
1164 | "icon": "01d"
1165 | }
1166 | ],
1167 | "clouds": {
1168 | "all": 0
1169 | },
1170 | "wind": {
1171 | "speed": 3.21,
1172 | "deg": 268.001
1173 | },
1174 | "rain": {},
1175 | "snow": {},
1176 | "sys": {
1177 | "pod": "d"
1178 | },
1179 | "dt_txt": "2017-02-20 15:00:00"
1180 | },
1181 | {
1182 | "dt": 1487613600,
1183 | "main": {
1184 | "temp": 274.443,
1185 | "temp_min": 274.443,
1186 | "temp_max": 274.443,
1187 | "pressure": 968.19,
1188 | "sea_level": 1042.66,
1189 | "grnd_level": 968.19,
1190 | "humidity": 88,
1191 | "temp_kf": 0
1192 | },
1193 | "weather": [
1194 | {
1195 | "id": 801,
1196 | "main": "Clouds",
1197 | "description": "few clouds",
1198 | "icon": "02n"
1199 | }
1200 | ],
1201 | "clouds": {
1202 | "all": 24
1203 | },
1204 | "wind": {
1205 | "speed": 3.27,
1206 | "deg": 257.501
1207 | },
1208 | "rain": {},
1209 | "snow": {},
1210 | "sys": {
1211 | "pod": "n"
1212 | },
1213 | "dt_txt": "2017-02-20 18:00:00"
1214 | },
1215 | {
1216 | "dt": 1487624400,
1217 | "main": {
1218 | "temp": 272.424,
1219 | "temp_min": 272.424,
1220 | "temp_max": 272.424,
1221 | "pressure": 968.38,
1222 | "sea_level": 1043.17,
1223 | "grnd_level": 968.38,
1224 | "humidity": 85,
1225 | "temp_kf": 0
1226 | },
1227 | "weather": [
1228 | {
1229 | "id": 801,
1230 | "main": "Clouds",
1231 | "description": "few clouds",
1232 | "icon": "02n"
1233 | }
1234 | ],
1235 | "clouds": {
1236 | "all": 20
1237 | },
1238 | "wind": {
1239 | "speed": 3.57,
1240 | "deg": 255.503
1241 | },
1242 | "rain": {},
1243 | "snow": {},
1244 | "sys": {
1245 | "pod": "n"
1246 | },
1247 | "dt_txt": "2017-02-20 21:00:00"
1248 | }
1249 | ],
1250 | "city": {
1251 | "id": 6940463,
1252 | "name": "Altstadt",
1253 | "coord": {
1254 | "lat": 48.137,
1255 | "lon": 11.5752
1256 | },
1257 | "country": "none"
1258 | }
1259 | }
--------------------------------------------------------------------------------