├── android
├── app
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── ic_launcher-playstore.png
│ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ │ ├── drawable
│ │ │ │ │ ├── ic_add.xml
│ │ │ │ │ ├── ic_sync.xml
│ │ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ │ ├── ic_t.xml
│ │ │ │ │ ├── ic_hc.xml
│ │ │ │ │ ├── ic_lr.xml
│ │ │ │ │ ├── ic_sl.xml
│ │ │ │ │ ├── ic_c.xml
│ │ │ │ │ ├── ic_lc.xml
│ │ │ │ │ ├── ic_h.xml
│ │ │ │ │ ├── ic_s.xml
│ │ │ │ │ ├── ic_hr.xml
│ │ │ │ │ ├── ic_sn.xml
│ │ │ │ │ └── ic_launcher_background.xml
│ │ │ │ ├── menu
│ │ │ │ │ └── action_menu.xml
│ │ │ │ └── layout
│ │ │ │ │ ├── activity_main.xml
│ │ │ │ │ ├── item_search_location.xml
│ │ │ │ │ └── item_location.xml
│ │ │ ├── swift
│ │ │ │ ├── .sourcery.yml
│ │ │ │ ├── Sources
│ │ │ │ │ └── WeatherCoreBridge
│ │ │ │ │ │ ├── SSLHelper.swift
│ │ │ │ │ │ ├── SwiftContainer.swift
│ │ │ │ │ │ └── Collection+JavaBridgeable.swift
│ │ │ │ ├── LinuxMain.swift
│ │ │ │ ├── .sourcery.LinuxMain.stencil
│ │ │ │ ├── Package.resolved
│ │ │ │ ├── Package.swift
│ │ │ │ └── .swiftlint.yml
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── readdle
│ │ │ │ │ └── weather
│ │ │ │ │ ├── core
│ │ │ │ │ ├── LocationWeatherData.kt
│ │ │ │ │ ├── Location.kt
│ │ │ │ │ ├── SSLHelper.kt
│ │ │ │ │ ├── LocationSearchViewModel.kt
│ │ │ │ │ ├── LocationSearchDelegateAndroid.kt
│ │ │ │ │ ├── LocationWeatherViewModelDelegateAndroid.kt
│ │ │ │ │ ├── Weather.kt
│ │ │ │ │ ├── LocationWeatherViewModel.kt
│ │ │ │ │ ├── WeatherState.kt
│ │ │ │ │ └── SwiftContainer.kt
│ │ │ │ │ ├── WeatherAppModule.kt
│ │ │ │ │ ├── WeatherApp.kt
│ │ │ │ │ ├── utils
│ │ │ │ │ └── AssetsManagerExt.kt
│ │ │ │ │ ├── adapters
│ │ │ │ │ ├── SearchLocationAdapter.kt
│ │ │ │ │ └── WeatherLocationAdapter.kt
│ │ │ │ │ ├── MainViewModel.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── readdle
│ │ │ │ └── weather
│ │ │ │ └── ExampleUnitTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── com
│ │ │ └── readdle
│ │ │ └── weather
│ │ │ └── ExampleInstrumentedTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── gradle
│ ├── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ └── libs.versions.toml
├── .gitignore
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradlew.bat
└── gradlew
├── doc
└── device-2020-04-21-000209.png
├── apple
├── Shared
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── ContentView.swift
│ └── SwiftWeatherApp.swift
├── SwiftWeather.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── macOS
│ ├── macOS.entitlements
│ └── Info.plist
├── Tests iOS
│ ├── Info.plist
│ └── Tests_iOS.swift
├── Tests macOS
│ ├── Info.plist
│ └── Tests_macOS.swift
└── iOS
│ └── Info.plist
├── core
├── .swiftpm
│ └── xcode
│ │ ├── package.xcworkspace
│ │ └── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── WeatherCore.xcscheme
├── .sourcery.yml
├── Sources
│ └── WeatherCore
│ │ ├── Data
│ │ ├── Location.swift
│ │ ├── WeatherState.swift
│ │ └── Weather.swift
│ │ ├── WeatherDatabase.swift
│ │ ├── WeatherProvider.swift
│ │ ├── WeatherCoreContainer.swift
│ │ ├── ViewModel
│ │ ├── LocationSearchViewModel.swift
│ │ └── LocationWeatherViewModel.swift
│ │ ├── OpenWeather
│ │ ├── OpenWeatherResponse.swift
│ │ └── OpenWeatherProvider.swift
│ │ └── JSONStorage
│ │ └── JSONStorage.swift
├── Package.swift
├── .sourcery.LinuxMain.stencil
└── Tests
│ └── WeatherCoreTests
│ ├── WeatherDatabaseTest.swift
│ ├── LocationSearchViewModelTest.swift
│ └── LocationWeatherViewModelTest.swift
├── .github
└── workflows
│ ├── mac-core.yml
│ ├── android-ui.yml
│ └── android-core.yml
├── .gitignore
├── LICENSE
└── README.md
/android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/doc/device-2020-04-21-000209.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/doc/device-2020-04-21-000209.png
--------------------------------------------------------------------------------
/apple/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Swift Weather
3 | Add
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriydruk/swift-weather-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/core/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/apple/SwiftWeather.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/apple/Shared/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/core/.sourcery.yml:
--------------------------------------------------------------------------------
1 | # This is config file for sourcery, just run `sourcery` in this folder
2 | sources:
3 | - Tests
4 | templates:
5 | - .sourcery.LinuxMain.stencil
6 | output:
7 | LinuxMain.swift
8 | args:
9 | testimports: '@testable import WeatherCoreTests'
10 |
--------------------------------------------------------------------------------
/.github/workflows/mac-core.yml:
--------------------------------------------------------------------------------
1 | name: MacCore
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: macOS-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Run swift tests
13 | run: cd core;
14 | swift test
15 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 18 19:20:34 EET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/.sourcery.yml:
--------------------------------------------------------------------------------
1 | # This is config file for sourcery, just run `sourcery` in this folder
2 | sources:
3 | - ../../../../../core/Tests
4 | templates:
5 | - .sourcery.LinuxMain.stencil
6 | output:
7 | LinuxMain.swift
8 | args:
9 | testimports: '@testable import WeatherCoreTests'
10 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/Data/Location.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | public struct Location: Codable, Hashable {
8 | public let woeId: Int64
9 | public let title: String
10 | public let latitude: Float
11 | public let longitude: Float
12 | }
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/apple/SwiftWeather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/Data/WeatherState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | public enum WeatherState: Int, Codable {
8 | case none
9 | case snow
10 | case thunderstorm
11 | case clear
12 | case drizzle
13 | case rain
14 | case clouds
15 | case atmosphere
16 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_add.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/WeatherDatabase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | protocol WeatherDatabase {
8 |
9 | func loadLocations() -> [Location]
10 |
11 | func addLocation(_ location: Location)
12 |
13 | func removeLocation(_ location: Location)
14 |
15 | func clearDB()
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/WeatherProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | protocol WeatherProvider {
8 |
9 | func searchLocations(query: String?, completionBlock: @escaping (Location?, Error?) -> ())
10 |
11 | func weather(location: Location, completionBlock: @escaping (Weather?, Error?) -> ())
12 | }
--------------------------------------------------------------------------------
/apple/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/LocationWeatherData.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import android.os.Parcelable
4 | import com.readdle.codegen.anotation.SwiftValue
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @SwiftValue @Parcelize
8 | data class LocationWeatherData(
9 | var location: Location = Location(),
10 | var weather: Weather? = null
11 | ): Parcelable
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/Location.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import android.os.Parcelable
4 | import com.readdle.codegen.anotation.SwiftValue
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @SwiftValue @Parcelize
8 | data class Location(
9 | var woeId: Long = 0,
10 | var title: String = "",
11 | var latitude: Float = 0.0f,
12 | var longitude: Float = 0.0f
13 | ): Parcelable
--------------------------------------------------------------------------------
/android/app/src/main/swift/Sources/WeatherCoreBridge/SSLHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 28.06.2020.
3 | //
4 |
5 | import Foundation
6 | import WeatherCore
7 |
8 | public class SSLHelper {
9 |
10 | public static func setupCert(basePath: String) {
11 | // Setup SSL
12 | let caPath = basePath + "/cacert.pem"
13 | setenv("URLSessionCertificateAuthorityInfoFile", caPath, 1)
14 | }
15 | }
--------------------------------------------------------------------------------
/apple/Shared/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Shared
4 | //
5 | // Created by Andrius Shiaulis on 04.07.2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 | var body: some View {
12 | Text("Hello, world!").padding()
13 | }
14 | }
15 |
16 | struct ContentView_Previews: PreviewProvider {
17 | static var previews: some View {
18 | ContentView()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/core/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "WeatherCore",
6 | products:[
7 | .library(
8 | name: "WeatherCore",
9 | targets:["WeatherCore"]
10 | )
11 | ],
12 | dependencies: [],
13 | targets: [
14 | .target(name: "WeatherCore"),
15 | .testTarget(name: "WeatherCoreTests", dependencies: ["WeatherCore"]),
16 | ]
17 | )
18 |
--------------------------------------------------------------------------------
/android/app/src/main/res/menu/action_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/android/app/src/test/java/com/readdle/weather/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_sync.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/Data/Weather.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | public struct Weather: Codable, Hashable {
8 | public let state: WeatherState
9 | public let date: Date
10 | public let minTemp: Float
11 | public let maxTemp: Float
12 | public let temp: Float
13 | public let windSpeed: Float
14 | public let windDirection: Float
15 | public let airPressure: Float
16 | public let humidity: Float
17 | public let visibility: Float
18 | public let predictability: Float
19 | }
20 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/SSLHelper.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core
2 |
3 | import com.readdle.codegen.anotation.SwiftFunc
4 | import com.readdle.codegen.anotation.SwiftReference
5 |
6 | @SwiftReference
7 | class SSLHelper private constructor() {
8 |
9 | // Swift JNI private native pointer
10 | private val nativePointer = 0L
11 |
12 | // Swift JNI release method
13 | external fun release()
14 |
15 | companion object {
16 | @JvmStatic @SwiftFunc("setupCert(basePath:)")
17 | external fun setupCert(basePath: String)
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/LocationSearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import com.readdle.codegen.anotation.SwiftFunc
4 | import com.readdle.codegen.anotation.SwiftReference
5 |
6 | @SwiftReference
7 | class LocationSearchViewModel private constructor() {
8 |
9 | // Swift JNI private native pointer
10 | private val nativePointer = 0L
11 |
12 | // Swift JNI release method
13 | external fun release()
14 |
15 | @SwiftFunc("searchLocations(query:)")
16 | external fun searchLocations(query: String?)
17 |
18 | companion object {
19 |
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/LocationSearchDelegateAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import com.readdle.codegen.anotation.SwiftCallbackFunc
4 | import com.readdle.codegen.anotation.SwiftDelegate
5 |
6 | @SwiftDelegate(protocols = ["LocationSearchDelegate"])
7 | interface LocationSearchDelegateAndroid {
8 |
9 | @SwiftCallbackFunc("onSuggestionStateChanged(state:)")
10 | fun onSuggestionStateChanged(state: ArrayList)
11 |
12 | @SwiftCallbackFunc("onError(errorDescription:)")
13 | fun onError(errorDescription: String)
14 |
15 | companion object {
16 |
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/LocationWeatherViewModelDelegateAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import com.readdle.codegen.anotation.SwiftCallbackFunc
4 | import com.readdle.codegen.anotation.SwiftDelegate
5 |
6 | @SwiftDelegate(protocols = ["LocationWeatherViewModelDelegate"])
7 | interface LocationWeatherViewModelDelegateAndroid {
8 |
9 | @SwiftCallbackFunc("onWeatherStateChanged(state:)")
10 | fun onWeatherStateChanged(state: ArrayList)
11 |
12 | @SwiftCallbackFunc("onError(errorDescription:)")
13 | fun onError(errorDescription: String)
14 |
15 | companion object {
16 |
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/WeatherAppModule.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather
2 |
3 | import android.app.Application
4 | import com.readdle.weather.core.SwiftContainer
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object WeatherAppModule {
14 |
15 | @Provides
16 | @Singleton
17 | fun weatherCoreContainer(application: Application): SwiftContainer {
18 | return SwiftContainer.init(application.dataDir.absolutePath, BuildConfig.WEATHER_API_KEY)
19 | }
20 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/WeatherApp.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather
2 |
3 | import android.app.Application
4 | import com.readdle.codegen.anotation.JavaSwift
5 | import com.readdle.weather.core.SSLHelper
6 | import com.readdle.weather.utils.copyAssetsIfNeeded
7 | import dagger.hilt.android.HiltAndroidApp
8 |
9 | @HiltAndroidApp
10 | class WeatherApp: Application() {
11 |
12 | override fun onCreate() {
13 | super.onCreate()
14 | System.loadLibrary("WeatherCoreBridge")
15 | JavaSwift.init()
16 | assets.copyAssetsIfNeeded("cacert.pem", dataDir.absolutePath)
17 | SSLHelper.setupCert(dataDir.absolutePath)
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/Weather.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import android.os.Parcelable
4 | import com.readdle.codegen.anotation.SwiftValue
5 | import kotlinx.parcelize.Parcelize
6 | import java.util.Date
7 |
8 | @SwiftValue @Parcelize
9 | data class Weather(
10 | var state: WeatherState = WeatherState.NONE,
11 | var date: Date = Date(),
12 | var minTemp: Float = 0.0f,
13 | var maxTemp: Float = 0.0f,
14 | var temp: Float = 0.0f,
15 | var windSpeed: Float = 0.0f,
16 | var windDirection: Float = 0.0f,
17 | var airPressure: Float = 0.0f,
18 | var humidity: Float = 0.0f,
19 | var visibility: Float = 0.0f,
20 | var predictability: Float = 0.0f
21 | ): Parcelable
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/LocationWeatherViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import com.readdle.codegen.anotation.SwiftFunc
4 | import com.readdle.codegen.anotation.SwiftReference
5 |
6 | @SwiftReference
7 | class LocationWeatherViewModel private constructor() {
8 |
9 | // Swift JNI private native pointer
10 | private val nativePointer = 0L
11 |
12 | // Swift JNI release method
13 | external fun release()
14 |
15 | @SwiftFunc("addLocationToSaved(location:)")
16 | external fun addLocationToSaved(location: Location)
17 |
18 | @SwiftFunc("removeSavedLocation(location:)")
19 | external fun removeSavedLocation(location: Location)
20 |
21 | companion object {
22 |
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/android/app/src/androidTest/java/com/readdle/weather/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.readdle.weather", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/Sources/WeatherCoreBridge/SwiftContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 13.09.2020.
3 | //
4 |
5 | import Foundation
6 | import WeatherCore
7 |
8 | public class SwiftContainer {
9 |
10 | private let container: WeatherCoreContainer
11 |
12 | public init(basePath: String, apiKey: String) {
13 | self.container = WeatherCoreContainer(basePath: basePath, apiKey: apiKey)
14 | }
15 |
16 | public func getWeatherViewModel(delegate: LocationWeatherViewModelDelegate) -> LocationWeatherViewModel {
17 | return container.getWeatherViewModel(delegate: delegate)
18 | }
19 |
20 | public func getLocationSearchViewModel(delegate: LocationSearchDelegate) -> LocationSearchViewModel {
21 | return container.getLocationSearchViewModel(delegate: delegate)
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/apple/Tests iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/apple/Tests macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/android/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | maven("https://s01.oss.sonatype.org/content/repositories/comreaddleswiftjavacodegen-1041/")
12 | gradlePluginPortal()
13 | }
14 | }
15 |
16 | dependencyResolutionManagement {
17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18 | repositories {
19 | google()
20 | mavenCentral()
21 | maven("https://s01.oss.sonatype.org/content/repositories/comreaddleswiftjavacodegen-1041/")
22 | }
23 | }
24 |
25 | rootProject.name = "Weather"
26 | include(":app")
27 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/WeatherCoreContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | public class WeatherCoreContainer {
8 |
9 | private let weatherProvider: WeatherProvider
10 | private let storage: WeatherDatabase
11 |
12 | public init(basePath: String, apiKey: String) {
13 | weatherProvider = OpenWeatherProvider(apiKey: apiKey)
14 | storage = JSONStorage(basePath: basePath)
15 | }
16 |
17 | public func getWeatherViewModel(delegate: LocationWeatherViewModelDelegate) -> LocationWeatherViewModel {
18 | return LocationWeatherViewModel(db: storage, provider: weatherProvider, delegate: delegate)
19 | }
20 |
21 | public func getLocationSearchViewModel(delegate: LocationSearchDelegate) -> LocationSearchViewModel {
22 | return LocationSearchViewModel(provider: weatherProvider, delegate: delegate)
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/WeatherState.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import android.os.Parcelable
4 | import com.readdle.codegen.anotation.SwiftValue
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @SwiftValue @Parcelize
8 | enum class WeatherState(val rawValue: Int): Parcelable {
9 |
10 | NONE(0),
11 | SNOW(1),
12 | THUNDERSTORM(2),
13 | SHOWERS(3),
14 | CLEAR(4),
15 | DRIZZLE(5),
16 | RAIN(6),
17 | CLOUDS(7),
18 | ATMOSPHERE(8);
19 |
20 | companion object {
21 |
22 | private val values = HashMap()
23 |
24 | @JvmStatic
25 | fun valueOf(rawValue: Int): WeatherState {
26 | return values[rawValue]!!
27 | }
28 |
29 | init {
30 | enumValues().forEach {
31 | values[it.rawValue] = it
32 | }
33 | }
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
4 | plugins {
5 | alias(libs.plugins.android.application) apply false
6 | alias(libs.plugins.kotlin.android) apply false
7 | }
8 |
9 | buildscript {
10 | repositories {
11 | // Repositories needed to find the plugin artifact itself
12 | google()
13 | mavenCentral() // The Readdle plugin artifact is here
14 | maven("https://s01.oss.sonatype.org/content/repositories/comreaddleswiftjavacodegen-1041/")
15 | }
16 | dependencies {
17 | // Declare the plugin classpath dependency using its real coordinates
18 | // Use the actual artifact name 'gradle' and the correct version '1.4.5'
19 | classpath(libs.gradle)
20 | classpath(libs.hilt.android.gradle.plugin)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/core/SwiftContainer.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.core;
2 |
3 | import com.readdle.codegen.anotation.SwiftFunc
4 | import com.readdle.codegen.anotation.SwiftReference
5 |
6 | @SwiftReference
7 | class SwiftContainer private constructor() {
8 |
9 | // Swift JNI private native pointer
10 | private val nativePointer = 0L
11 |
12 | // Swift JNI release method
13 | external fun release()
14 |
15 | @SwiftFunc("getWeatherViewModel(delegate:)")
16 | external fun getWeatherViewModel(delegate: LocationWeatherViewModelDelegateAndroid): LocationWeatherViewModel
17 |
18 | @SwiftFunc("getLocationSearchViewModel(delegate:)")
19 | external fun getLocationSearchViewModel(delegate: LocationSearchDelegateAndroid): LocationSearchViewModel
20 |
21 | companion object {
22 | @JvmStatic @SwiftFunc("init(basePath:apiKey:)")
23 | external fun init(basePath: String, apiKey: String): SwiftContainer
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/apple/macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | android/.idea
16 | android/app/src/main/jniLibs
17 | android/app/src/main/swift/.build
18 | android/app/src/main/swift/.idea
19 | core/.build
20 | android/app/src/main/swift/WeatherCoreBridge.xcodeproj
21 | core/.swiftpm/xcode/xcuserdata
22 | core/.swiftpm/xcode/package.xcworkspace/xcuserdata
23 | apple/Weather.xcodeproj/project.xcworkspace/xcuserdata
24 | apple/Weather.xcodeproj/xcuserdata
25 | *.xcuserstate
26 | *.xcbkptlist
27 |
28 | apple/SwiftWeather.xcodeproj/xcuserdata/andrius.xcuserdatad/xcschemes/xcschememanagement.plist
29 | apple/SwiftWeather.xcodeproj/xcuserdata
30 | core/WeatherCore.xcodeproj/xcshareddata
31 | core/WeatherCore.xcodeproj/project.xcworkspace/xcshareddata
32 | Package.resolved
33 | .idea
34 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_t.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Andriy Druk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apple/Shared/SwiftWeatherApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftWeatherApp.swift
3 | // Shared
4 | //
5 | // Created by Andrius Shiaulis on 04.07.2020.
6 | //
7 |
8 | import SwiftUI
9 | import WeatherCore
10 |
11 | @main
12 | struct SwiftWeatherApp: App, LocationWeatherViewModelDelegate, LocationSearchDelegate {
13 |
14 | let container = WeatherCoreContainer(basePath: Bundle.main.bundlePath)
15 |
16 | var weatherViewModel: LocationWeatherViewModel?
17 | var searchViewModel: LocationSearchViewModel?
18 |
19 | var body: some Scene {
20 | WindowGroup {
21 | ContentView()
22 | }
23 | }
24 |
25 | init() {
26 | weatherViewModel = container.getWeatherViewModel(delegate: self)
27 | searchViewModel = container.getLocationSearchViewModel(delegate: self)
28 | }
29 |
30 | func onWeatherStateChanged(state: [LocationWeatherData]) {
31 |
32 | }
33 |
34 | func onSuggestionStateChanged(state: [Location]) {
35 |
36 | }
37 |
38 | func onError(errorDescription: String) {
39 | NSLog("Error: %s", errorDescription)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | // Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery
2 | // DO NOT EDIT
3 |
4 |
5 | import XCTest
6 | @testable import WeatherCoreTests
7 |
8 |
9 |
10 | extension WeatherDatabaseTest {
11 | static var allTests = [
12 | ("testDefaults", testDefaults),
13 | ("testAddLocation", testAddLocation),
14 | ("testRemoveLocation", testRemoveLocation),
15 | ("testClearLocation", testClearLocation),
16 | ]
17 |
18 | }
19 |
20 | extension WeatherRepositoryTest {
21 | static var allTests = [
22 | ("testLoadSavedLocations", testLoadSavedLocations),
23 | ("testAddLocationToSaved", testAddLocationToSaved),
24 | ("testRemoveSavedLocation", testRemoveSavedLocation),
25 | ("testSearchLocationsSuccessful", testSearchLocationsSuccessful),
26 | ("testSearchLocationsFail", testSearchLocationsFail),
27 | ("testWeatherSuccessful", testWeatherSuccessful),
28 | ("testWeatherFail", testWeatherFail),
29 | ]
30 |
31 | }
32 |
33 |
34 | XCTMain([
35 | testCase(WeatherDatabaseTest.allTests),
36 | testCase(WeatherRepositoryTest.allTests),
37 | ])
38 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_hc.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/item_search_location.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
23 |
24 |
31 |
32 |
--------------------------------------------------------------------------------
/.github/workflows/android-ui.yml:
--------------------------------------------------------------------------------
1 | name: AndroidApp
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: macOS-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Install Swift 6.0.3 toolchain
13 | run: curl -L -O https://download.swift.org/swift-6.0.3-release/xcode/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-osx.pkg && sudo installer -pkg swift-6.0.3-RELEASE-osx.pkg -target /
14 | - name: Install NDK
15 | run: $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "ndk;26.2.11394342"
16 | - name: Install Swift Android Toolchain
17 | run: wget https://github.com/readdle/swift-android-toolchain/releases/latest/download/swift-android.zip;
18 | unzip swift-android.zip;
19 | - uses: actions/setup-java@v3
20 | with:
21 | distribution: 'zulu' # See 'Supported distributions' for available options
22 | java-version: '17'
23 | - name: Build all variants
24 | run: export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.2.11394342;
25 | export SWIFT_ANDROID_HOME=$(pwd)/swift-android;
26 | export PATH=$ANDROID_NDK_HOME:$PATH;
27 | export PATH=$SWIFT_ANDROID_HOME/build-tools:$PATH;
28 | cd android;
29 | echo "API_KEY=XXXYYYZZZ" >> local.properties;
30 | ./gradlew app:buildRelease
31 |
--------------------------------------------------------------------------------
/core/.sourcery.LinuxMain.stencil:
--------------------------------------------------------------------------------
1 | import XCTest
2 | {{ argument.testimports }}
3 |
4 |
5 |
6 | {% for type in types.classes|based:"XCTestCase" %}
7 | {% if not type.annotations.disableTests and not type.annotations.notUnitTests %}extension {{ type.name }} {
8 | static var allTests = [
9 | {% if argument.test_type == "unit-testing" %}
10 | {% for method in type.methods %}{% if not method.annotations.disableTest and not method.annotations.notUnitTest and method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{ method.shortName }}),
11 | {% endif %}{% endfor %}
12 | {% else %}
13 | {% for method in type.methods %}{% if not method.annotations.disableTest and method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{ method.shortName }}),
14 | {% endif %}{% endfor %}
15 | {% endif %}
16 | ]
17 |
18 | }
19 |
20 | {% endif %}{% endfor %}
21 |
22 | XCTMain([
23 | {% for type in types.classes|based:"XCTestCase" %}
24 | {% if not type.annotations.disableTests and not type.annotations.notUnitTests %}
25 | {% if argument.test_type == "unit-testing" %}
26 | {% if not type.annotations.notUnitTest %}
27 | testCase({{ type.name }}.allTests),
28 | {% endif %}
29 | {% else %}
30 | testCase({{ type.name }}.allTests),
31 | {% endif %}{% endif %}
32 | {% endfor %}])
33 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/.sourcery.LinuxMain.stencil:
--------------------------------------------------------------------------------
1 | import XCTest
2 | {{ argument.testimports }}
3 |
4 |
5 |
6 | {% for type in types.classes|based:"XCTestCase" %}
7 | {% if not type.annotations.disableTests and not type.annotations.notUnitTests %}extension {{ type.name }} {
8 | static var allTests = [
9 | {% if argument.test_type == "unit-testing" %}
10 | {% for method in type.methods %}{% if not method.annotations.disableTest and not method.annotations.notUnitTest and method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{ method.shortName }}),
11 | {% endif %}{% endfor %}
12 | {% else %}
13 | {% for method in type.methods %}{% if not method.annotations.disableTest and method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{ method.shortName }}),
14 | {% endif %}{% endfor %}
15 | {% endif %}
16 | ]
17 |
18 | }
19 |
20 | {% endif %}{% endfor %}
21 |
22 | XCTMain([
23 | {% for type in types.classes|based:"XCTestCase" %}
24 | {% if not type.annotations.disableTests and not type.annotations.notUnitTests %}
25 | {% if argument.test_type == "unit-testing" %}
26 | {% if not type.annotations.notUnitTest %}
27 | testCase({{ type.name }}.allTests),
28 | {% endif %}
29 | {% else %}
30 | testCase({{ type.name }}.allTests),
31 | {% endif %}{% endif %}
32 | {% endfor %}])
33 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/utils/AssetsManagerExt.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.utils
2 |
3 | import android.content.res.AssetManager
4 | import android.util.Log
5 | import java.io.*
6 |
7 | fun AssetManager.copyAssetsIfNeeded(filename: String, destination: String) {
8 | var inputStream: InputStream? = null
9 | var outputStream: OutputStream? = null
10 | try {
11 | val outFile = File(destination, filename)
12 | if (outFile.exists()) {
13 | return
14 | }
15 | val parent = outFile.parentFile
16 | if (parent != null && !parent.exists() && !parent.mkdirs()) {
17 | Log.e("WeatherApp", "Couldn't create dir: $parent")
18 | return
19 | }
20 | inputStream = open(filename)
21 | outputStream = FileOutputStream(outFile)
22 | val buffer = ByteArray(1024)
23 | var read: Int
24 | while (inputStream.read(buffer).also { read = it } != -1) {
25 | outputStream.write(buffer, 0, read)
26 | }
27 | } catch (e: IOException) {
28 | Log.e("WeatherApp", "Failed to copy asset file: $filename", e)
29 | } finally {
30 | fun closeSilently(closeable: Closeable?) {
31 | if (closeable != null) {
32 | try {
33 | closeable.close()
34 | } catch (e: IOException) {
35 | e.printStackTrace()
36 | }
37 | }
38 | }
39 | closeSilently(inputStream)
40 | closeSilently(outputStream)
41 | }
42 | }
--------------------------------------------------------------------------------
/apple/Tests iOS/Tests_iOS.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests_iOS.swift
3 | // Tests iOS
4 | //
5 | // Created by Andrius Shiaulis on 04.07.2020.
6 | //
7 |
8 | import XCTest
9 |
10 | class Tests_iOS: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apple/Tests macOS/Tests_macOS.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests_macOS.swift
3 | // Tests macOS
4 | //
5 | // Created by Andrius Shiaulis on 04.07.2020.
6 | //
7 |
8 | import XCTest
9 |
10 | class Tests_macOS: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/core/Tests/WeatherCoreTests/WeatherDatabaseTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import WeatherCore
3 |
4 | #if os(Android)
5 | let temporaryDirectory = "/data/local/tmp"
6 | #else
7 | let temporaryDirectory = NSTemporaryDirectory()
8 | #endif
9 |
10 | class WeatherDatabaseTest: XCTestCase {
11 |
12 | private var database: WeatherDatabase!
13 |
14 | override func setUp() {
15 | super.setUp()
16 | database = JSONStorage(basePath: temporaryDirectory)
17 | database.clearDB()
18 | }
19 |
20 | func testDefaults() {
21 | let defaultLocations = database.loadLocations()
22 | XCTAssert(defaultLocations.count == 3)
23 | }
24 |
25 | func testAddLocation() {
26 | database.addLocation(Location(woeId: 0, title: "Fake", latitude: 0.0, longitude: 0.0))
27 | let locations = database.loadLocations()
28 | XCTAssert(locations.count == 4)
29 | XCTAssertNotNil(locations.first(where: { $0.woeId == 0}))
30 | }
31 |
32 | func testRemoveLocation() {
33 | guard let firstLocation = database.loadLocations().first else {
34 | XCTFail()
35 | return
36 | }
37 | database.removeLocation(firstLocation)
38 | XCTAssert(database.loadLocations().count == 2)
39 | }
40 |
41 | func testClearLocation() {
42 | database.addLocation(Location(woeId: 0, title: "Fake", latitude: 0.0, longitude: 0.0))
43 | database.clearDB()
44 | let locations = database.loadLocations()
45 | XCTAssert(locations.count == 3)
46 | XCTAssertNil(locations.first(where: { $0.woeId == 0}))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/android-core.yml:
--------------------------------------------------------------------------------
1 | name: AndroidCore
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: macos-13
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Install Swift 6.0.3 toolchain
13 | run: curl -L -O https://download.swift.org/swift-6.0.3-release/xcode/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-osx.pkg && sudo installer -pkg swift-6.0.3-RELEASE-osx.pkg -target /
14 | - name: Install NDK
15 | run: $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "ndk;26.2.11394342"
16 | - name: Install Swift Android Toolchain
17 | run: wget https://github.com/readdle/swift-android-toolchain/releases/latest/download/swift-android.zip;
18 | unzip swift-android.zip;
19 | - name: Download Android Emulator
20 | run: $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "system-images;android-29;google_apis;x86_64"
21 | - name: Create Android Emulator
22 | run: $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager create avd -n ci-test -k "system-images;android-29;google_apis;x86_64" -d "pixel"
23 | - name: Start Android Emulator
24 | run: $ANDROID_HOME/emulator/emulator -avd ci-test -no-window -noaudio -no-boot-anim -no-snapshot -read-only -gpu off > /dev/null &
25 | - name: Run connected android tests
26 | run: export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.2.11394342;
27 | export SWIFT_ANDROID_ARCH=x86_64;
28 | export SWIFT_ANDROID_HOME=$(pwd)/swift-android;
29 | export PATH=$ANDROID_NDK_HOME:$SWIFT_ANDROID_HOME/build-tools:$PATH;
30 | adb wait-for-device;
31 | cd core;
32 | swift android test
33 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/adapters/SearchLocationAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.adapters
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.TextView
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.readdle.weather.R
9 | import com.readdle.weather.core.Location
10 |
11 | class SearchLocationAdapter(private var locations: List,
12 | private var onClickListener: (Location) -> Unit) :
13 | RecyclerView.Adapter() {
14 |
15 | class LocationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
16 | val titleText: TextView = itemView.findViewById(R.id.title)
17 | }
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup,
20 | viewType: Int): LocationViewHolder {
21 | val textView = LayoutInflater.from(parent.context)
22 | .inflate(R.layout.item_search_location, parent, false)
23 | return LocationViewHolder(
24 | textView
25 | )
26 | }
27 |
28 | override fun onBindViewHolder(holder: LocationViewHolder, position: Int) {
29 | val location = locations[position]
30 | holder.titleText.text = location.title
31 | holder.itemView.setOnClickListener {
32 | onClickListener(location)
33 | }
34 | }
35 |
36 | override fun getItemCount() = locations.size
37 |
38 | fun swapLocations(locations: List) {
39 | this.locations = locations
40 | notifyDataSetChanged()
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/android/app/src/main/swift/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "java_swift",
6 | "repositoryURL": "https://github.com/readdle/java_swift.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "8c6910dbc94657e5dfb2bc9d1d4d862ffd809d83",
10 | "version": "2.2.3"
11 | }
12 | },
13 | {
14 | "package": "AndroidNDK",
15 | "repositoryURL": "https://github.com/readdle/swift-android-ndk.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "0fbe0b28344b75079ff5d811551727bdd4ab8ec0",
19 | "version": "1.1.4"
20 | }
21 | },
22 | {
23 | "package": "AnyCodable",
24 | "repositoryURL": "https://github.com/readdle/swift-anycodable.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "9163d43ad317d938629592bbae798a95a3a0a4cf",
28 | "version": "1.0.3"
29 | }
30 | },
31 | {
32 | "package": "Java",
33 | "repositoryURL": "https://github.com/readdle/swift-java.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "e9449162891ce8e9687e9ff87d44c0752556df49",
37 | "version": "0.3.0"
38 | }
39 | },
40 | {
41 | "package": "JavaCoder",
42 | "repositoryURL": "https://github.com/readdle/swift-java-coder.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "d46cfd9f0efb61e45b08a01160a9e4aa35ae4817",
46 | "version": "1.1.2"
47 | }
48 | }
49 | ]
50 | },
51 | "version": 1
52 | }
53 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_lr.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
21 |
22 |
23 |
25 |
29 |
30 |
31 |
33 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/ViewModel/LocationSearchViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 01.09.2020.
3 | //
4 |
5 | import Foundation
6 | import Dispatch
7 |
8 | public protocol LocationSearchDelegate {
9 | func onSuggestionStateChanged(state: [Location])
10 | func onError(errorDescription: String)
11 | }
12 |
13 | private let DEFAULT_TIMEOUT_REQUEST = 0.1 // 100ms
14 |
15 | public class LocationSearchViewModel {
16 |
17 | private let provider: WeatherProvider
18 | private let delegate: LocationSearchDelegate
19 |
20 | private var dispatchWorkItem: DispatchWorkItem?
21 |
22 | init(provider: WeatherProvider, delegate: LocationSearchDelegate) {
23 | self.provider = provider
24 | self.delegate = delegate
25 | }
26 |
27 | public func searchLocations(query: String?) {
28 | self.dispatchWorkItem?.cancel()
29 | dispatchWorkItem = DispatchWorkItem(block: {
30 | self.provider.searchLocations(query: query) { [weak self] location, error in
31 | guard let strongSelf = self else {
32 | return
33 | }
34 | if let error = error {
35 | strongSelf.delegate.onError(errorDescription: error.localizedDescription)
36 | }
37 | else if let location = location {
38 | strongSelf.delegate.onSuggestionStateChanged(state: [location])
39 | }
40 | }
41 | })
42 | if let dispatchWorkItem = dispatchWorkItem {
43 | DispatchQueue.global().asyncAfter(deadline: .now() + DEFAULT_TIMEOUT_REQUEST, execute: dispatchWorkItem)
44 | }
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/apple/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/Sources/WeatherCoreBridge/Collection+JavaBridgeable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andrew on 1/11/18.
3 | //
4 |
5 | #if os(Android)
6 |
7 | import Foundation
8 | import java_swift
9 | import JavaCoder
10 |
11 | private let AndroidPackage = "com/readdle/weather/core"
12 |
13 | public extension Array {
14 |
15 | // Decoding SwiftValue type with JavaCoder
16 | static func from(javaObject: jobject) throws -> Array where T: Decodable {
17 | // ignore forPackage for basic impl
18 | return try JavaDecoder(forPackage: AndroidPackage, missingFieldsStrategy: .throw).decode(Array.self, from: javaObject)
19 | }
20 |
21 | }
22 |
23 | public extension Array where Element: Encodable {
24 |
25 | // Encoding SwiftValue type with JavaCoder
26 | func javaObject() throws -> jobject {
27 | // ignore forPackage for basic impl
28 | return try JavaEncoder(forPackage: AndroidPackage, missingFieldsStrategy: .throw).encode(self)
29 | }
30 |
31 | }
32 |
33 | public extension Dictionary {
34 |
35 | // Decoding SwiftValue type with JavaCoder
36 | static func from(javaObject: jobject) throws -> Dictionary where K: Decodable, V: Decodable {
37 | // ignore forPackage for basic impl
38 | return try JavaDecoder(forPackage: AndroidPackage, missingFieldsStrategy: .throw).decode(Dictionary.self, from: javaObject)
39 | }
40 |
41 | }
42 |
43 | public extension Dictionary where Key: Encodable, Value: Encodable {
44 |
45 | // Encoding SwiftValue type with JavaCoder
46 | func javaObject() throws -> jobject {
47 | // ignore forPackage for basic impl
48 | return try JavaEncoder(forPackage: AndroidPackage, missingFieldsStrategy: .throw).encode(self)
49 | }
50 |
51 | }
52 |
53 | #endif
54 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/item_location.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
28 |
29 |
40 |
41 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_sl.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
16 |
19 |
20 |
21 |
23 |
26 |
27 |
28 |
30 |
34 |
35 |
36 |
38 |
41 |
42 |
43 |
45 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_c.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/OpenWeather/OpenWeatherResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Maksym Sutkovenko on 11.07.2022.
3 | //
4 |
5 | import Foundation
6 |
7 | struct OpenWeatherResponse: Codable {
8 | let coord: OpenWeatherLocation
9 | let weather: [OpenWeather]
10 | let main: OpenWeatherMain
11 | let visibility: Int
12 | let wind: OpenWeatherWind
13 | let id: Int
14 | let name: String
15 | }
16 |
17 | struct OpenWeatherLocation: Codable {
18 | let lon: Float
19 | let lat: Float
20 | }
21 |
22 | struct OpenWeather: Codable {
23 | let id: Int
24 | let main: String
25 | }
26 |
27 | struct OpenWeatherMain: Codable {
28 | let temp: Float
29 | let temp_min: Float
30 | let temp_max: Float
31 | let pressure: Int
32 | let humidity: Int
33 | }
34 |
35 | struct OpenWeatherWind: Codable {
36 | let speed: Float
37 | let deg: Int
38 | }
39 |
40 | extension OpenWeatherResponse {
41 |
42 | func toWeather() -> Weather {
43 | Weather(state: weather.first?.main.toWeatherState() ?? .none,
44 | date: Date(),
45 | minTemp: main.temp_min,
46 | maxTemp: main.temp_max,
47 | temp: main.temp,
48 | windSpeed: wind.speed,
49 | windDirection: Float(wind.deg),
50 | airPressure: Float(main.pressure),
51 | humidity: Float(main.humidity),
52 | visibility: Float(visibility),
53 | predictability: Float(visibility))
54 | }
55 |
56 | func toLocation() -> Location {
57 | Location(woeId: Int64(id), title: name, latitude: coord.lat, longitude: coord.lon)
58 | }
59 | }
60 |
61 | fileprivate extension String {
62 |
63 | func toWeatherState() -> WeatherState {
64 | switch self {
65 | case "Thunderstorm": return .thunderstorm
66 | case "Drizzle": return .drizzle
67 | case "Rain": return .rain
68 | case "Snow": return .snow
69 | case "Atmosphere": return .atmosphere
70 | case "Clear": return .clear
71 | case "Clouds": return .clouds
72 | default: return .none
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/android/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.9.1"
3 | gradle = "6.0.3"
4 | hiltAndroidGradlePlugin = "2.56.1"
5 | hiltVersion = "2.56.1"
6 | kotlin = "2.0.21"
7 | coreKtx = "1.15.0"
8 | junit = "4.13.2"
9 | junitVersion = "1.2.1"
10 | espressoCore = "3.6.1"
11 | appcompat = "1.7.0"
12 | material = "1.12.0"
13 | activity = "1.10.1"
14 | constraintlayout = "2.2.1"
15 | swift = "6.0.3"
16 | codegen = "0.9.6"
17 |
18 | [libraries]
19 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
20 | gradle = { module = "com.readdle.android.swift:gradle", version.ref = "gradle" }
21 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltVersion" }
22 | hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hiltAndroidGradlePlugin" }
23 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltVersion" }
24 | junit = { group = "junit", name = "junit", version.ref = "junit" }
25 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
26 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
27 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
28 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
29 | androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
30 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
31 | swift-codegen = { module = "com.readdle.swift.java.codegen:compiler", version.ref = "codegen" }
32 | swift-codegen-annotations = { module = "com.readdle.swift.java.codegen:annotations", version.ref = "codegen" }
33 |
34 | [plugins]
35 | android-application = { id = "com.android.application", version.ref = "agp" }
36 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
37 | readdle-swift = { id = "com.readdle.android.swift", version.ref = "swift" }
38 | hilt = { id = "com.google.dagger", version.ref = "hiltAndroidGradlePlugin" }
39 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | import Foundation
3 | import PackageDescription
4 |
5 | let packageName = "WeatherCoreBridge"
6 |
7 | // generated sources integration
8 | let generatedName = "Generated"
9 | let generatedPath = ".build/\(generatedName.lowercased())"
10 |
11 | let isSourcesGenerated: Bool = {
12 | let baseURL = URL(fileURLWithPath: #file).deletingLastPathComponent()
13 | let generatedURL = baseURL.appendingPathComponent(generatedPath)
14 |
15 | var isDirectory: ObjCBool = false
16 | let exists = FileManager.default.fileExists(atPath: generatedURL.path, isDirectory: &isDirectory)
17 |
18 | return exists && isDirectory.boolValue
19 | }()
20 |
21 | func addGenerated(_ products: [Product]) -> [Product] {
22 | if isSourcesGenerated == false {
23 | return products
24 | }
25 |
26 | return products + [
27 | .library(name: packageName, type: .dynamic, targets: [generatedName])
28 | ]
29 | }
30 |
31 | func addGenerated(_ targets: [Target]) -> [Target] {
32 | if isSourcesGenerated == false {
33 | return targets
34 | }
35 |
36 | return targets + [
37 | .target(
38 | name: generatedName,
39 | dependencies: [
40 | .byName(name: packageName),
41 | "java_swift",
42 | "Java",
43 | "JavaCoder",
44 | ],
45 | path: generatedPath
46 | )
47 | ]
48 | }
49 | // generated sources integration end
50 |
51 | let package = Package(
52 | name: packageName,
53 | products: addGenerated([
54 | ]),
55 | dependencies: [
56 | .package(url: "https://github.com/readdle/java_swift.git", .exact("2.2.3")),
57 | .package(url: "https://github.com/readdle/swift-java.git", .exact("0.3.0")),
58 | .package(url: "https://github.com/readdle/swift-java-coder.git", .exact("1.1.2")),
59 | .package(url: "https://github.com/readdle/swift-anycodable.git", .exact("1.0.3")),
60 | .package(path: "../../../../../core")
61 | ],
62 | targets: addGenerated([
63 | .target(name: packageName, dependencies: ["AnyCodable", "java_swift", "JavaCoder", "WeatherCore"])
64 | ])
65 | )
66 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_lc.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import com.readdle.weather.core.*
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class MainViewModel @Inject constructor(
12 | container: SwiftContainer
13 | ) : ViewModel(), LocationWeatherViewModelDelegateAndroid, LocationSearchDelegateAndroid {
14 |
15 | private val locationWeatherViewModel = container.getWeatherViewModel(this)
16 | private val locationSearchViewModel = container.getLocationSearchViewModel(this)
17 |
18 | private val weatherLiveData = MutableLiveData>()
19 | private val searchSuggestionLiveData = MutableLiveData>()
20 | private val errorDescriptionLiveData = MutableLiveData()
21 |
22 | override fun onCleared() {
23 | super.onCleared()
24 | locationWeatherViewModel.release()
25 | locationSearchViewModel.release()
26 | }
27 |
28 | fun getWeatherLiveData() : LiveData> {
29 | return weatherLiveData
30 | }
31 |
32 | fun getSearchSuggestionLiveData() : LiveData> {
33 | return searchSuggestionLiveData
34 | }
35 |
36 | fun getErrorDescriptionLiveData() : LiveData {
37 | return errorDescriptionLiveData
38 | }
39 |
40 | override fun onSuggestionStateChanged(state: ArrayList) {
41 | searchSuggestionLiveData.postValue(state)
42 | }
43 |
44 | override fun onWeatherStateChanged(state: ArrayList) {
45 | weatherLiveData.postValue(state)
46 | }
47 |
48 | override fun onError(errorDescription: String) {
49 | errorDescriptionLiveData.postValue(errorDescription)
50 | }
51 |
52 | fun searchLocations(newText: String?) {
53 | locationSearchViewModel.searchLocations(newText)
54 | }
55 |
56 | fun addLocation(location: Location) {
57 | locationWeatherViewModel.addLocationToSaved(location)
58 | }
59 |
60 | fun removeLocation(location: Location) {
61 | locationWeatherViewModel.removeSavedLocation(location)
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_h.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
16 |
19 |
20 |
21 |
23 |
26 |
27 |
28 |
30 |
34 |
35 |
36 |
38 |
41 |
42 |
43 |
45 |
48 |
49 |
50 |
52 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🇺🇦 #StandWithUkraine
2 | On Feb. 24, 2022 Russia declared an [unprovoked war on Ukraine](https://war.ukraine.ua/russia-war-crimes/) and launched a full-scale invasion. Russia is currently bombing peaceful Ukrainian cities, including schools and hospitals and attacking civilians who are fleeing conflict zones.
3 |
4 | Please support Ukraine by lobbying your governments, protesting peacefully, and donating money to support the people of Ukraine. Below are links to trustworthy organizations that are helping to defend Ukraine in this unprovoked war:
5 |
6 | * [Donate to Come Back Alive](https://www.comebackalive.in.ua/donate)
7 | * [Donate to KOLO](https://koloua.com/en/)
8 | * [Donate to Prytula Foundation](https://prytulafoundation.org/en)
9 |
10 | # Swift Weather App  
11 | Cross-platform Swift application
12 |
13 | 
14 |
15 | ## Arhitecture
16 |
17 | Architecture based on reusing as much as possible code written on Swift. Currently, Swift Weather Core includes weather repository that handles loading info from the database and fetching new data from providers.
18 |
19 | ```
20 | ------------------------------------------
21 | / \ \
22 | +---------+ +---------------------+ +-----------+ \ +---------+ \ +----------+
23 | | macOS |<-->| Swift Weather Core |<-->| Android | ->| iOS | ->| Windows |
24 | +---------+ +---------------------+ +-----------+ +---------+ +----------+
25 | | Weather repository |
26 | +---------------------+
27 | | Weather database |
28 | +---------------------+
29 | | Weather provider |
30 | +---------------------+
31 | ```
32 |
33 |
34 |
35 | ## How to build [Android]
36 |
37 | For building an Android application you need [Readdle's Swift Android Toolchain](https://github.com/readdle/swift-android-toolchain#installation). Please follow the guideline on installation first.
38 | After a successful setup, you can clone this repo and build it with Android Studio as any other android project.
39 |
40 |
41 | ## How to build [iOS/MacOS]
42 |
43 | For building an iOS application you need [Xcode 12](https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_12_beta/Xcode_12_beta.xip). Minimal target version of operation systems is iOS 14 and MacOS 11.
44 |
45 |
46 | ## How to build [Windows]
47 |
48 | In progress...
49 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_s.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
21 |
22 |
23 |
25 |
29 |
30 |
31 |
33 |
37 |
38 |
39 |
41 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/core/Tests/WeatherCoreTests/LocationSearchViewModelTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import WeatherCore
3 |
4 | class LocationSearchViewModelTest: XCTestCase {
5 |
6 | func testSearchLocationsSuccessful() {
7 | let suggestionExpectation = expectation(description: "onSearchSuggestionChanged should be called")
8 | let repository = createViewModel(suggestionExpectation: suggestionExpectation)
9 | repository.searchLocations(query: "")
10 | wait(for: [suggestionExpectation], timeout: 1.0)
11 | }
12 |
13 | func testSearchLocationsFail() {
14 | let errorExpectation = expectation(description: "onError should be called")
15 | let repository = createViewModel(errorExpectation: errorExpectation)
16 | repository.searchLocations(query: nil)
17 | wait(for: [errorExpectation], timeout: 1.0)
18 | }
19 |
20 | func createViewModel(suggestionExpectation: XCTestExpectation? = nil,
21 | errorExpectation: XCTestExpectation? = nil) -> LocationSearchViewModel {
22 | let fakeProvider = FakeProvider()
23 | let fakeDelegate = FakeWeatherRepositoryDelegate(suggestionExpectation: suggestionExpectation, errorExpectation: errorExpectation)
24 | return LocationSearchViewModel(provider: fakeProvider, delegate: fakeDelegate)
25 | }
26 |
27 | struct FakeProvider: WeatherProvider {
28 |
29 | func searchLocations(query: String?, completionBlock: @escaping (Location?, Error?) -> ()) {
30 | if query != nil {
31 | completionBlock(fakeLocation, nil)
32 | }
33 | else {
34 | completionBlock(nil, NSError(domain: "", code: 0))
35 | }
36 | }
37 |
38 | func weather(location: Location, completionBlock: @escaping (Weather?, Error?) -> ()) {
39 | if location.woeId == 0 {
40 | completionBlock(fakeWeather, nil)
41 | }
42 | else {
43 | completionBlock(nil, NSError(domain: "", code: 0))
44 | }
45 | }
46 | }
47 |
48 | struct FakeWeatherRepositoryDelegate: LocationSearchDelegate {
49 |
50 | let suggestionExpectation: XCTestExpectation?
51 | let errorExpectation: XCTestExpectation?
52 |
53 | func onSuggestionStateChanged(state: [Location]) {
54 | XCTAssertEqual(state.first?.woeId, fakeLocation.woeId)
55 | suggestionExpectation?.fulfill()
56 | }
57 |
58 | func onError(errorDescription: String) {
59 | #if os(Android)
60 | XCTAssertEqual(errorDescription, "The operation could not be completed. ( error 0.)")
61 | #else
62 | XCTAssertEqual(errorDescription, "The operation couldn’t be completed. ( error 0.)")
63 | #endif
64 | errorExpectation?.fulfill()
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_hr.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
21 |
22 |
23 |
25 |
29 |
30 |
31 |
33 |
37 |
38 |
39 |
41 |
45 |
46 |
47 |
49 |
53 |
54 |
55 |
57 |
61 |
62 |
63 |
65 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/android/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | plugins {
4 | alias(libs.plugins.android.application)
5 | alias(libs.plugins.kotlin.android)
6 | id("kotlin-kapt")
7 | id("kotlin-parcelize")
8 | id("com.readdle.android.swift")
9 | id("dagger.hilt.android.plugin")
10 | }
11 |
12 | swift {
13 | useKapt = true
14 | cleanEnabled = false
15 | swiftLintEnabled = false
16 | apiLevel = 24
17 | }
18 |
19 | val props = Properties()
20 | val localPropertiesFile = rootProject.file("local.properties") // Or just file("local.properties") if in the same module's build script
21 | if (localPropertiesFile.exists()) {
22 | localPropertiesFile.inputStream().use { props.load(it) }
23 | }
24 | val openWeatherAPIKey: String = props.getProperty("API_KEY") ?: ""
25 |
26 | android {
27 | namespace = "com.readdle.weather"
28 | compileSdk = 35
29 |
30 | defaultConfig {
31 | applicationId = "com.readdle.weather"
32 | minSdk = 24
33 | targetSdk = 35
34 | versionCode = 1
35 | versionName = "1.0"
36 |
37 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
38 |
39 | buildConfigField("String", "WEATHER_API_KEY", "\"${openWeatherAPIKey}\"")
40 | }
41 |
42 | buildTypes {
43 | release {
44 | isDebuggable = false
45 | isJniDebuggable = false
46 | }
47 | debug {
48 | isDebuggable = true
49 | isJniDebuggable = true
50 | ndk {
51 | // Optimize for fast build times by not building all ABIs for debuggable variants.
52 | //noinspection ChromeOsAbiSupport
53 | abiFilters += listOf("arm64-v8a")
54 | }
55 | }
56 | }
57 |
58 | compileOptions {
59 | sourceCompatibility = JavaVersion.VERSION_11
60 | targetCompatibility = JavaVersion.VERSION_11
61 | }
62 |
63 | kotlinOptions {
64 | jvmTarget = "11"
65 | }
66 |
67 | buildFeatures {
68 | buildConfig = true
69 | }
70 | }
71 |
72 | kapt {
73 | arguments {
74 | arg("com.readdle.codegen.package", """{
75 | "moduleName": "WeatherCoreBridgeGenerated",
76 | "importPackages": ["AnyCodable", "JavaCoder", "WeatherCore", "WeatherCoreBridge"]
77 | }""")
78 | }
79 | }
80 |
81 | dependencies {
82 | kapt(libs.hilt.compiler)
83 | kapt(libs.swift.codegen)
84 |
85 | implementation(libs.hilt.android)
86 | implementation(libs.swift.codegen.annotations)
87 | implementation(libs.androidx.core.ktx)
88 | implementation(libs.androidx.appcompat)
89 | implementation(libs.material)
90 | implementation(libs.androidx.activity)
91 | implementation(libs.androidx.constraintlayout)
92 | testImplementation(libs.junit)
93 | androidTestImplementation(libs.androidx.junit)
94 | androidTestImplementation(libs.androidx.espresso.core)
95 | }
--------------------------------------------------------------------------------
/core/.swiftpm/xcode/xcshareddata/xcschemes/WeatherCore.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/apple/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | },
93 | {
94 | "idiom" : "mac",
95 | "scale" : "1x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "2x",
101 | "size" : "16x16"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "1x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "2x",
111 | "size" : "32x32"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "1x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "2x",
121 | "size" : "128x128"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "1x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "2x",
131 | "size" : "256x256"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "1x",
136 | "size" : "512x512"
137 | },
138 | {
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "512x512"
142 | }
143 | ],
144 | "info" : {
145 | "author" : "xcode",
146 | "version" : 1
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/JSONStorage/JSONStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | // Fake DB with JSON persistence
8 | // TODO: replace with SQLite
9 | class JSONStorage: WeatherDatabase {
10 |
11 | private static let FILENAME = "db.json"
12 |
13 | private var locationsCache: [Location]?
14 | private var basePath: String
15 |
16 | public init(basePath: String) {
17 | self.basePath = basePath
18 | }
19 |
20 | public func loadLocations() -> [Location] {
21 | if locationsCache == nil {
22 | loadFromDisk()
23 | }
24 | return locationsCache ?? []
25 | }
26 |
27 | public func addLocation(_ location: Location) {
28 | if locationsCache == nil {
29 | loadFromDisk()
30 | }
31 | let contains = locationsCache?.contains(where: { $0.woeId == location.woeId })
32 | if contains == false {
33 | locationsCache?.append(location)
34 | saveOnDisk()
35 | }
36 | }
37 |
38 | public func removeLocation(_ location: Location) {
39 | if locationsCache == nil {
40 | loadFromDisk()
41 | }
42 | locationsCache?.removeAll(where: { $0.woeId == location.woeId })
43 | saveOnDisk()
44 | }
45 |
46 | public func clearDB() {
47 | let fileURL = URL(fileURLWithPath: basePath).appendingPathComponent(JSONStorage.FILENAME)
48 | if FileManager.default.fileExists(atPath: fileURL.path) {
49 | do {
50 | try FileManager.default.removeItem(at: fileURL)
51 | locationsCache = nil
52 | }
53 | catch {
54 | NSLog("Can't remove file \(fileURL), error: \(error.localizedDescription)")
55 | }
56 | }
57 | }
58 |
59 | private func loadFromDisk() {
60 | let fileURL = URL(fileURLWithPath: basePath).appendingPathComponent(JSONStorage.FILENAME)
61 | if FileManager.default.fileExists(atPath: fileURL.path) {
62 | do {
63 | let data = try Data(contentsOf: fileURL)
64 | locationsCache = try JSONDecoder().decode([Location].self, from: data)
65 | return
66 | }
67 | catch {
68 | NSLog("loading error: \(error.localizedDescription)")
69 | }
70 | }
71 |
72 | // Default locations: Kiev, Berlin, San Fransisco
73 | locationsCache = [
74 | Location(woeId: 924938, title: "Kyiv", latitude: 50.441380, longitude: 30.522490),
75 | Location(woeId: 638242, title: "Berlin", latitude: 52.516071, longitude: 13.376980),
76 | Location(woeId: 2487956, title: "San Francisco", latitude: 37.77712, longitude: -122.41964)
77 | ]
78 | }
79 |
80 | private func saveOnDisk() {
81 | let fileURL = URL(fileURLWithPath: basePath).appendingPathComponent(JSONStorage.FILENAME)
82 | let locations: [Location] = locationsCache ?? []
83 | do {
84 | let data = try JSONEncoder().encode(locations)
85 | try data.write(to: fileURL)
86 | }
87 | catch {
88 | NSLog("saving error: \(error) to file \(fileURL)")
89 | }
90 | }
91 |
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/OpenWeather/OpenWeatherProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 | #if canImport(FoundationNetworking)
7 | import FoundationNetworking
8 | #endif
9 |
10 | class OpenWeatherProvider: WeatherProvider {
11 |
12 | let sessionConfig = URLSessionConfiguration.default
13 | let session: URLSession
14 | private let apiKey: String
15 |
16 | var searchDataTask: URLSessionDataTask?
17 |
18 | init(apiKey: String) {
19 | self.session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
20 | self.apiKey = apiKey
21 | }
22 |
23 | func searchLocations(query: String?, completionBlock: @escaping (Location?, Error?) -> ()) {
24 | searchDataTask?.cancel()
25 | guard let query = query, query.count > 0 else {
26 | completionBlock(nil, nil)
27 | return
28 | }
29 | if var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather") {
30 | urlComponents.query = "q=\(query)&appid=\(apiKey)"
31 | guard let url = urlComponents.url else {
32 | return
33 | }
34 | searchDataTask = session.dataTask(with: url) { [weak self] data, response, error in
35 | defer {
36 | self?.searchDataTask = nil
37 | }
38 | do {
39 | let result = try OpenWeatherProvider.parseResponse(OpenWeatherResponse.self, data: data, response: response, error: error)
40 | completionBlock(result?.toLocation(), nil)
41 | }
42 | catch {
43 | completionBlock(nil, error)
44 | }
45 | }
46 | searchDataTask?.resume()
47 | }
48 | }
49 |
50 | func weather(location: Location, completionBlock: @escaping (Weather?, Error?) -> ()) {
51 | if let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=\(location.latitude)&lon=\(location.longitude)&units=metric&appid=\(apiKey)") {
52 | let task = session.dataTask(with: url) { data, response, error in
53 | do {
54 | let result = try OpenWeatherProvider.parseResponse(OpenWeatherResponse.self, data: data, response: response, error: error)
55 | completionBlock(result?.toWeather() ?? nil, nil)
56 | }
57 | catch {
58 | completionBlock(nil, error)
59 | }
60 | }
61 | task.resume()
62 | }
63 | }
64 |
65 | private static func parseResponse(_ type: T.Type, data: Data?, response: URLResponse?, error: Error?) throws -> T? where T : Decodable {
66 | if let error = error {
67 | let urlError = error as NSError
68 | if urlError.domain == URLError.errorDomain, urlError.code == URLError.cancelled.rawValue {
69 | return nil // If task cancelled -> skip error
70 | }
71 | throw error
72 | } else if let data = data,
73 | let response = response as? HTTPURLResponse,
74 | response.statusCode == 200 {
75 | do {
76 | return try JSONDecoder().decode(type, from: data)
77 | }
78 | catch let decodeError {
79 | throw decodeError
80 | }
81 | } else {
82 | return nil
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_sn.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
17 |
20 |
21 |
22 |
24 |
27 |
28 |
29 |
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/android/app/src/main/swift/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - identifier_name #short vars
3 | - force_cast #exclude tests, shoule be enabled #TODO
4 | - force_try #exclude tests
5 | - type_name
6 | - type_body_length
7 | - statement_position #}else{
8 | - extension_access_modifier #not bad #TODO
9 | - file_header #check details
10 | - attributes #not bad #TODO
11 | - nesting #?
12 | - todo #todo
13 | - file_length
14 | - for_where #modern swift #TODO
15 | - syntactic_sugar #modern swift #TODO
16 | - function_body_length #short funcs not bad #TODO - 70
17 | - trailing_whitespace #xcode default
18 | # - vertical_parameter_alignment_on_call #strange in swift
19 | - cyclomatic_complexity #shoule be enabled #TODO2
20 | - pattern_matching_keywords #hz
21 | - override_in_extension #hz
22 | - unused_closure_parameter #i think it looks better with names
23 | - trailing_comma
24 | - large_tuple #looks good for me
25 |
26 | - compiler_protocol_init #TODO
27 | - weak_delegate #TODO
28 | - class_delegate_protocol #TODO
29 | - redundant_nil_coalescing #intruduces bugs
30 |
31 | - function_parameter_count #?
32 | # - closure_parameter_position
33 | - multiple_closures_with_trailing_closure
34 | - is_disjoint #?
35 | - contains_over_first_not_nil #maybe
36 | - no_fallthrough_only #?
37 | - notification_center_detachment
38 | - redundant_string_enum_value
39 | opt_in_rules:
40 | # - array_init
41 | - attributes
42 | - closure_end_indentation
43 | - closure_spacing
44 | - contains_over_first_not_nil
45 | # - empty_count #TODO2
46 | - explicit_init
47 | - extension_access_modifier
48 | - file_header
49 | # - file_name #not bad
50 | - first_where
51 | - joined_default_parameter
52 | - let_var_whitespace
53 | - literal_expression_end_indentation
54 | - nimble_operator
55 | - number_separator
56 | - object_literal
57 | - operator_usage_whitespace
58 | - overridden_super_call
59 | - override_in_extension
60 | - pattern_matching_keywords
61 | - private_action
62 | - private_outlet
63 | - prohibited_super_call
64 | - quick_discouraged_call
65 | - quick_discouraged_focused_test
66 | - quick_discouraged_pending_test
67 | - redundant_nil_coalescing
68 | - single_test_class
69 | - sorted_first_last
70 | - sorted_imports
71 | - unneeded_parentheses_in_closure_argument
72 | # - vertical_parameter_alignment_on_call
73 | # - yoda_condition
74 | - brackets_statment
75 | - closure_inner_space
76 | - brackets_space
77 | excluded:
78 | - Pods
79 | - .build
80 |
81 | type_name:
82 | min_length: 1 # only warning
83 | max_length: # warning and error
84 | warning: 40
85 | error: 50
86 | excluded: iPhone # excluded via string
87 | identifier_name:
88 | min_length: # only min_length
89 | error: 1 # only error
90 | excluded: # excluded via string array
91 | - id
92 | - URL
93 | - GlobalAPIKey
94 | line_length: 1000 #160
95 | number_separator:
96 | minimum_length: 5
97 | custom_rules:
98 | brackets_statment:
99 | name: Brackets Statment
100 | message: statments after } shoule be started from new line
101 | regex: \}[ ]*(if|else|catch)
102 | severity: error
103 | brackets_space:
104 | name: Block Opening
105 | message: shoule be whitespace after {
106 | regex: \{(?:\(|\w)
107 | severity: error
108 | match_kinds: # SyntaxKinds to match. optional.
109 | - parameter
110 | - identifier
111 | closure_inner_space:
112 | name: Closure Inner Space
113 | message: closures should have space after {
114 | regex: \{\w+(?:, \w+)* in\b
115 | severity: warning
116 |
117 | #TODO todo should have tickect refrence
118 |
--------------------------------------------------------------------------------
/core/Sources/WeatherCore/ViewModel/LocationWeatherViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Andriy Druk on 20.04.2020.
3 | //
4 |
5 | import Foundation
6 |
7 | public protocol LocationWeatherViewModelDelegate {
8 | func onWeatherStateChanged(state: [LocationWeatherData])
9 | func onError(errorDescription: String)
10 | }
11 |
12 | public struct LocationWeatherData: Codable {
13 | public let location: Location
14 | public let weather: Weather?
15 | }
16 |
17 | public class LocationWeatherViewModel {
18 |
19 | private let provider: WeatherProvider
20 | private let db: WeatherDatabase
21 | private let delegate: LocationWeatherViewModelDelegate
22 |
23 | private let dbQueue = OperationQueue()
24 |
25 | private var stateLock = NSLock()
26 | private var state = [Int64: LocationWeatherData]()
27 |
28 | init(db: WeatherDatabase, provider: WeatherProvider, delegate: LocationWeatherViewModelDelegate) {
29 | self.db = db
30 | self.provider = provider
31 | self.delegate = delegate
32 |
33 | self.dbQueue.maxConcurrentOperationCount = 1
34 | dbQueue.addOperation { [weak self] in
35 | guard let strongSelf = self else {
36 | return
37 | }
38 | let locations = strongSelf.db.loadLocations()
39 | strongSelf.addLocations(locations: locations)
40 | }
41 | }
42 |
43 | public func addLocationToSaved(location: Location) {
44 | dbQueue.addOperation { [weak self] in
45 | guard let strongSelf = self else {
46 | return
47 | }
48 | strongSelf.db.addLocation(location)
49 | strongSelf.addLocations(locations: [location])
50 | }
51 | }
52 |
53 | public func removeSavedLocation(location: Location) {
54 | dbQueue.addOperation { [weak self] in
55 | guard let strongSelf = self else {
56 | return
57 | }
58 | strongSelf.db.removeLocation(location)
59 | strongSelf.removeLocation(woeId: location.woeId)
60 | }
61 | }
62 |
63 | private func addLocations(locations: [Location]) {
64 | modifyState {
65 | locations.forEach {
66 | state[$0.woeId] = LocationWeatherData(location: $0, weather: nil)
67 | }
68 | }
69 | locations.forEach {
70 | weather(location: $0)
71 | }
72 | }
73 |
74 | private func removeLocation(woeId: Int64) {
75 | modifyState {
76 | state[woeId] = nil
77 | }
78 | }
79 |
80 | private func updateWeather(woeId: Int64, weather: Weather) {
81 | modifyState {
82 | if let oldValue = state.removeValue(forKey: woeId) {
83 | state[woeId] = LocationWeatherData(location: oldValue.location, weather: weather)
84 | }
85 | }
86 | }
87 |
88 | private func modifyState(block: ()->Void) {
89 | stateLock.lock()
90 | block()
91 | stateLock.unlock()
92 | let currentState = state.values.sorted(by: { $0.location.title > $1.location.title })
93 | delegate.onWeatherStateChanged(state: currentState)
94 | }
95 |
96 | func weather(location: Location) {
97 | provider.weather(location: location){ [weak self] weather, error in
98 | guard let strongSelf = self else {
99 | return
100 | }
101 | if let error = error {
102 | strongSelf.delegate.onError(errorDescription: error.localizedDescription)
103 | }
104 | else if let weather = weather {
105 | strongSelf.updateWeather(woeId: Int64(location.woeId), weather: weather)
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/adapters/WeatherLocationAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather.adapters
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.ImageView
7 | import android.widget.ProgressBar
8 | import android.widget.TextView
9 | import androidx.annotation.DrawableRes
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.readdle.weather.R
12 | import com.readdle.weather.core.Location
13 | import com.readdle.weather.core.LocationWeatherData
14 | import com.readdle.weather.core.WeatherState
15 | import kotlin.math.round
16 |
17 | class WeatherLocationAdapter(private var weathers: List,
18 | private var onLongClickListener: (Location) -> Unit) :
19 | RecyclerView.Adapter() {
20 |
21 | // Provide a reference to the views for each data item
22 | // Complex data items may need more than one view per item, and
23 | // you provide access to all the views for a data item in a view holder.
24 | // Each data item is just a string in this case that is shown in a TextView.
25 | class LocationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
26 | val weatherState: ImageView = itemView.findViewById(R.id.weatherState)
27 | val titleText: TextView = itemView.findViewById(R.id.title)
28 | val tempText: TextView = itemView.findViewById(R.id.temp)
29 | val progress: ProgressBar = itemView.findViewById(R.id.loading)
30 | }
31 |
32 |
33 | // Create new views (invoked by the layout manager)
34 | override fun onCreateViewHolder(parent: ViewGroup,
35 | viewType: Int): LocationViewHolder {
36 | val textView = LayoutInflater.from(parent.context)
37 | .inflate(R.layout.item_location, parent, false)
38 | return LocationViewHolder(
39 | textView
40 | )
41 | }
42 |
43 | // Replace the contents of a view (invoked by the layout manager)
44 | override fun onBindViewHolder(holder: LocationViewHolder, position: Int) {
45 | // - get element from your dataset at this position
46 | // - replace the contents of the view with that element
47 |
48 | val location = weathers[position].location
49 | holder.titleText.text = location.title
50 | val weather = weathers[position].weather
51 | if (weather != null) {
52 | holder.weatherState.setImageResource(getDrawableForWeatherState(weather.state))
53 | holder.tempText.text = "${round(weather.temp).toInt()} °C"
54 | holder.progress.visibility = View.GONE
55 | holder.weatherState.visibility = View.VISIBLE
56 | }
57 | else {
58 | holder.progress.visibility = View.VISIBLE
59 | holder.weatherState.visibility = View.GONE
60 | holder.tempText.text = "-- °C"
61 | }
62 | holder.itemView.setOnLongClickListener {
63 | onLongClickListener(location)
64 | return@setOnLongClickListener true
65 | }
66 | }
67 |
68 | // Return the size of your dataset (invoked by the layout manager)
69 | override fun getItemCount() = weathers.size
70 |
71 | fun swapWeathers(weathers: List) {
72 | this.weathers = weathers
73 | notifyDataSetChanged()
74 | }
75 |
76 | @DrawableRes
77 | private fun getDrawableForWeatherState(state: WeatherState): Int {
78 | return when (state) {
79 | WeatherState.NONE -> R.drawable.ic_sync
80 | WeatherState.SNOW -> R.drawable.ic_sn
81 | WeatherState.THUNDERSTORM -> R.drawable.ic_t
82 | WeatherState.SHOWERS -> R.drawable.ic_s
83 | WeatherState.CLEAR -> R.drawable.ic_c
84 | WeatherState.DRIZZLE -> R.drawable.ic_lr
85 | WeatherState.RAIN -> R.drawable.ic_hr
86 | WeatherState.CLOUDS -> R.drawable.ic_hc
87 | WeatherState.ATMOSPHERE -> R.drawable.ic_lc
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/readdle/weather/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.readdle.weather
2 |
3 | import android.os.Bundle
4 | import android.view.Menu
5 | import android.view.MenuItem
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AlertDialog
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.appcompat.widget.SearchView
10 | import androidx.core.view.ViewCompat
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import androidx.recyclerview.widget.RecyclerView
13 | import com.google.android.material.snackbar.Snackbar
14 | import com.readdle.weather.adapters.SearchLocationAdapter
15 | import com.readdle.weather.adapters.WeatherLocationAdapter
16 | import com.readdle.weather.core.Location
17 | import dagger.hilt.android.AndroidEntryPoint
18 | import androidx.core.view.WindowCompat
19 | import androidx.core.view.WindowInsetsCompat
20 | import androidx.core.view.updatePadding
21 |
22 | @AndroidEntryPoint
23 | class MainActivity : AppCompatActivity() {
24 |
25 | private lateinit var recycler: RecyclerView
26 | private lateinit var weatherLocationAdapter: WeatherLocationAdapter
27 | private lateinit var searchLocationAdapter: SearchLocationAdapter
28 |
29 | private val model: MainViewModel by viewModels()
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | WindowCompat.setDecorFitsSystemWindows(window, true)
34 | setContentView(R.layout.activity_main)
35 |
36 | val viewManager = LinearLayoutManager(this)
37 | weatherLocationAdapter = WeatherLocationAdapter(emptyList()) {
38 | removeLocation(it)
39 | }
40 |
41 | recycler = findViewById(R.id.recycler_view).apply {
42 | // use this setting to improve performance if you know that changes
43 | // in content do not change the layout size of the RecyclerView
44 | setHasFixedSize(true)
45 |
46 | // use a linear layout manager
47 | layoutManager = viewManager
48 |
49 | // specify an viewAdapter (see also next example)
50 | adapter = weatherLocationAdapter
51 | }
52 |
53 | ViewCompat.setOnApplyWindowInsetsListener(recycler) { view, windowInsets ->
54 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
55 | view.updatePadding(top = insets.top)
56 | windowInsets
57 | }
58 |
59 | model.getWeatherLiveData().observe(this) {
60 | weatherLocationAdapter.swapWeathers(it)
61 | }
62 | model.getSearchSuggestionLiveData().observe(this) {
63 | searchLocationAdapter.swapLocations(it)
64 | }
65 | model.getErrorDescriptionLiveData().observe(this) {
66 | Snackbar.make(recycler, it, Snackbar.LENGTH_SHORT).show()
67 | }
68 | }
69 |
70 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
71 | val inflater = menuInflater
72 | inflater.inflate(R.menu.action_menu, menu)
73 | val searchViewItem: MenuItem = menu.findItem(R.id.app_bar_search)
74 | val searchView: SearchView = searchViewItem.actionView as SearchView
75 | searchView.setOnCloseListener {
76 | recycler.adapter = weatherLocationAdapter
77 | return@setOnCloseListener false
78 | }
79 | searchView.setOnSearchClickListener {
80 | recycler.adapter = searchLocationAdapter
81 | }
82 |
83 | searchLocationAdapter = SearchLocationAdapter(emptyList()) {
84 | searchView.onActionViewCollapsed()
85 | recycler.adapter = weatherLocationAdapter
86 | addLocation(it)
87 | }
88 |
89 | searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
90 |
91 | override fun onQueryTextChange(newText: String?): Boolean {
92 | model.searchLocations(newText)
93 | return true
94 | }
95 |
96 | override fun onQueryTextSubmit(query: String?): Boolean {
97 | return true
98 | }
99 |
100 | })
101 | return super.onCreateOptionsMenu(menu)
102 | }
103 |
104 | private fun addLocation(location: Location) {
105 | model.addLocation(location)
106 | Snackbar.make(recycler, location.title + " was added", Snackbar.LENGTH_SHORT).show()
107 | }
108 |
109 | private fun removeLocation(location: Location) {
110 | AlertDialog.Builder(this)
111 | .setMessage("Are you sure you want to delete " + location.title + " from saved?")
112 | .setPositiveButton("Yes") { dialog, _ ->
113 | model.removeLocation(location)
114 | Snackbar.make(recycler, location.title + " was removed", Snackbar.LENGTH_SHORT).show()
115 | dialog.dismiss()
116 | }
117 | .setNegativeButton("No") { dialog, _ ->
118 | dialog.dismiss()
119 | }
120 | .show()
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/core/Tests/WeatherCoreTests/LocationWeatherViewModelTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import WeatherCore
3 |
4 | let fakeLocation = Location(woeId: 0, title: "Fake", latitude: 0.0, longitude: 0.0)
5 | let fakeWeather = Weather(state: .clear, date: Date(), minTemp: 0.0, maxTemp: 0.0,
6 | temp: 0.0, windSpeed: 0.0, windDirection: 0.0, airPressure: 0.0,
7 | humidity: 0.0, visibility: 0.0, predictability: 0.0)
8 |
9 | class LocationWeatherViewModelTest: XCTestCase {
10 |
11 | var viewModel: LocationWeatherViewModel?
12 |
13 | func testLoadSavedLocations() {
14 | let weatherExpectation = expectation(description: "onWeatherChanged should be called")
15 | viewModel = createViewModel(weatherExpectation: weatherExpectation)
16 | wait(for: [weatherExpectation], timeout: 1.0)
17 | XCTAssert(viewModel != nil)
18 | }
19 |
20 | func testAddLocationToSaved() {
21 | let addExpectation = expectation(description: "addLocation should be called")
22 | viewModel = createViewModel(addExpectation: addExpectation)
23 | viewModel?.addLocationToSaved(location: fakeLocation)
24 | wait(for: [addExpectation], timeout: 1.0)
25 | }
26 |
27 | func testRemoveSavedLocation() {
28 | let removeExpectation = expectation(description: "removeLocation should be called")
29 | viewModel = createViewModel(removeExpectation: removeExpectation)
30 | viewModel?.removeSavedLocation(location: fakeLocation)
31 | wait(for: [removeExpectation], timeout: 1.0)
32 | }
33 |
34 | func testWeatherFail() {
35 | let errorExpectation = expectation(description: "onError should be called")
36 | viewModel = createViewModel(errorExpectation: errorExpectation)
37 | viewModel?.weather(location: Location(woeId: 1, title: "fake", latitude: 0.0, longitude: 0.0))
38 | wait(for: [errorExpectation], timeout: 1.0)
39 | }
40 |
41 | func createViewModel(addExpectation: XCTestExpectation? = nil,
42 | removeExpectation: XCTestExpectation? = nil,
43 | weatherExpectation: XCTestExpectation? = nil,
44 | errorExpectation: XCTestExpectation? = nil) -> LocationWeatherViewModel {
45 | let fakeDatabase = FakeDatabase(addExpectation: addExpectation, removeExpectation: removeExpectation)
46 | let fakeProvider = FakeProvider()
47 | let fakeDelegate = FakeWeatherRepositoryDelegate(weatherExpectation: weatherExpectation, errorExpectation: errorExpectation)
48 | return LocationWeatherViewModel(db: fakeDatabase, provider: fakeProvider, delegate: fakeDelegate)
49 | }
50 |
51 | struct FakeDatabase: WeatherDatabase {
52 |
53 | let addExpectation: XCTestExpectation?
54 | let removeExpectation: XCTestExpectation?
55 |
56 | func loadLocations() -> [Location] {
57 | return [fakeLocation]
58 | }
59 |
60 | func addLocation(_ location: Location) {
61 | XCTAssertEqual(location, fakeLocation)
62 | addExpectation?.fulfill()
63 | }
64 |
65 | func removeLocation(_ location: Location) {
66 | XCTAssertEqual(location, fakeLocation)
67 | removeExpectation?.fulfill()
68 | }
69 |
70 | func clearDB() { }
71 |
72 | }
73 |
74 | struct FakeProvider: WeatherProvider {
75 |
76 | func searchLocations(query: String?, completionBlock: @escaping (Location?, Error?) -> ()) {
77 | if query != nil {
78 | completionBlock(fakeLocation, nil)
79 | }
80 | else {
81 | completionBlock(nil, NSError(domain: "", code: 0))
82 | }
83 | }
84 |
85 | func weather(location: Location, completionBlock: @escaping (Weather?, Error?) -> ()) {
86 | if location.woeId == 0 {
87 | completionBlock(fakeWeather, nil)
88 | }
89 | else {
90 | completionBlock(nil, NSError(domain: "", code: 0))
91 | }
92 | }
93 | }
94 |
95 | struct FakeWeatherRepositoryDelegate: LocationWeatherViewModelDelegate {
96 |
97 | let weatherExpectation: XCTestExpectation?
98 | let errorExpectation: XCTestExpectation?
99 |
100 | func onWeatherStateChanged(state: [LocationWeatherData]) {
101 | if state.count > 0,
102 | let first = state.first,
103 | let weather = first.weather {
104 | XCTAssertEqual(first.location.woeId, fakeLocation.woeId)
105 | XCTAssertEqual(weather, fakeWeather)
106 | weatherExpectation?.fulfill()
107 | }
108 | }
109 |
110 | func onError(errorDescription: String) {
111 | #if os(Android)
112 | XCTAssertEqual(errorDescription, "The operation could not be completed. ( error 0.)")
113 | #else
114 | XCTAssertEqual(errorDescription, "The operation couldn’t be completed. ( error 0.)")
115 | #endif
116 | errorExpectation?.fulfill()
117 | }
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/apple/SwiftWeather.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 09B25AE224B0C4A4004DFBA0 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B25AE124B0C4A4004DFBA0 /* Tests_iOS.swift */; };
11 | 09B25AED24B0C4A4004DFBA0 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B25AEC24B0C4A4004DFBA0 /* Tests_macOS.swift */; };
12 | 09B25AEF24B0C4A4004DFBA0 /* SwiftWeatherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B25AC624B0C4A3004DFBA0 /* SwiftWeatherApp.swift */; };
13 | 09B25AF024B0C4A4004DFBA0 /* SwiftWeatherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B25AC624B0C4A3004DFBA0 /* SwiftWeatherApp.swift */; };
14 | 09B25AF124B0C4A4004DFBA0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B25AC724B0C4A3004DFBA0 /* ContentView.swift */; };
15 | 09B25AF224B0C4A4004DFBA0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B25AC724B0C4A3004DFBA0 /* ContentView.swift */; };
16 | 09B25AF324B0C4A4004DFBA0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09B25AC824B0C4A4004DFBA0 /* Assets.xcassets */; };
17 | 09B25AF424B0C4A4004DFBA0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09B25AC824B0C4A4004DFBA0 /* Assets.xcassets */; };
18 | 84E895AC24F10FB0000A82E6 /* WeatherCore in Frameworks */ = {isa = PBXBuildFile; productRef = 84E895AB24F10FB0000A82E6 /* WeatherCore */; };
19 | 84E895AE24F10FBA000A82E6 /* WeatherCore in Frameworks */ = {isa = PBXBuildFile; productRef = 84E895AD24F10FBA000A82E6 /* WeatherCore */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXContainerItemProxy section */
23 | 09B25ADE24B0C4A4004DFBA0 /* PBXContainerItemProxy */ = {
24 | isa = PBXContainerItemProxy;
25 | containerPortal = 09B25AC124B0C4A3004DFBA0 /* Project object */;
26 | proxyType = 1;
27 | remoteGlobalIDString = 09B25ACC24B0C4A4004DFBA0;
28 | remoteInfo = iOS;
29 | };
30 | 09B25AE924B0C4A4004DFBA0 /* PBXContainerItemProxy */ = {
31 | isa = PBXContainerItemProxy;
32 | containerPortal = 09B25AC124B0C4A3004DFBA0 /* Project object */;
33 | proxyType = 1;
34 | remoteGlobalIDString = 09B25AD424B0C4A4004DFBA0;
35 | remoteInfo = macOS;
36 | };
37 | /* End PBXContainerItemProxy section */
38 |
39 | /* Begin PBXFileReference section */
40 | 09B25AC624B0C4A3004DFBA0 /* SwiftWeatherApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftWeatherApp.swift; sourceTree = ""; };
41 | 09B25AC724B0C4A3004DFBA0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
42 | 09B25AC824B0C4A4004DFBA0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
43 | 09B25ACD24B0C4A4004DFBA0 /* SwiftWeather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftWeather.app; sourceTree = BUILT_PRODUCTS_DIR; };
44 | 09B25AD024B0C4A4004DFBA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
45 | 09B25AD524B0C4A4004DFBA0 /* SwiftWeather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftWeather.app; sourceTree = BUILT_PRODUCTS_DIR; };
46 | 09B25AD724B0C4A4004DFBA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
47 | 09B25AD824B0C4A4004DFBA0 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
48 | 09B25ADD24B0C4A4004DFBA0 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
49 | 09B25AE124B0C4A4004DFBA0 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; };
50 | 09B25AE324B0C4A4004DFBA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
51 | 09B25AE824B0C4A4004DFBA0 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
52 | 09B25AEC24B0C4A4004DFBA0 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; };
53 | 09B25AEE24B0C4A4004DFBA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
54 | 84E895A824F10D19000A82E6 /* core */ = {isa = PBXFileReference; lastKnownFileType = folder; name = core; path = ../core; sourceTree = ""; };
55 | /* End PBXFileReference section */
56 |
57 | /* Begin PBXFrameworksBuildPhase section */
58 | 09B25ACA24B0C4A4004DFBA0 /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | 84E895AE24F10FBA000A82E6 /* WeatherCore in Frameworks */,
63 | );
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | 09B25AD224B0C4A4004DFBA0 /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | 84E895AC24F10FB0000A82E6 /* WeatherCore in Frameworks */,
71 | );
72 | runOnlyForDeploymentPostprocessing = 0;
73 | };
74 | 09B25ADA24B0C4A4004DFBA0 /* Frameworks */ = {
75 | isa = PBXFrameworksBuildPhase;
76 | buildActionMask = 2147483647;
77 | files = (
78 | );
79 | runOnlyForDeploymentPostprocessing = 0;
80 | };
81 | 09B25AE524B0C4A4004DFBA0 /* Frameworks */ = {
82 | isa = PBXFrameworksBuildPhase;
83 | buildActionMask = 2147483647;
84 | files = (
85 | );
86 | runOnlyForDeploymentPostprocessing = 0;
87 | };
88 | /* End PBXFrameworksBuildPhase section */
89 |
90 | /* Begin PBXGroup section */
91 | 09B25AC024B0C4A3004DFBA0 = {
92 | isa = PBXGroup;
93 | children = (
94 | 84E895A824F10D19000A82E6 /* core */,
95 | 09B25AC524B0C4A3004DFBA0 /* Shared */,
96 | 09B25ACF24B0C4A4004DFBA0 /* iOS */,
97 | 09B25AD624B0C4A4004DFBA0 /* macOS */,
98 | 09B25AE024B0C4A4004DFBA0 /* Tests iOS */,
99 | 09B25AEB24B0C4A4004DFBA0 /* Tests macOS */,
100 | 09B25ACE24B0C4A4004DFBA0 /* Products */,
101 | 849CD2AE24BF72580096E6E5 /* Frameworks */,
102 | );
103 | sourceTree = "";
104 | };
105 | 09B25AC524B0C4A3004DFBA0 /* Shared */ = {
106 | isa = PBXGroup;
107 | children = (
108 | 09B25AC624B0C4A3004DFBA0 /* SwiftWeatherApp.swift */,
109 | 09B25AC724B0C4A3004DFBA0 /* ContentView.swift */,
110 | 09B25AC824B0C4A4004DFBA0 /* Assets.xcassets */,
111 | );
112 | path = Shared;
113 | sourceTree = "";
114 | };
115 | 09B25ACE24B0C4A4004DFBA0 /* Products */ = {
116 | isa = PBXGroup;
117 | children = (
118 | 09B25ACD24B0C4A4004DFBA0 /* SwiftWeather.app */,
119 | 09B25AD524B0C4A4004DFBA0 /* SwiftWeather.app */,
120 | 09B25ADD24B0C4A4004DFBA0 /* Tests iOS.xctest */,
121 | 09B25AE824B0C4A4004DFBA0 /* Tests macOS.xctest */,
122 | );
123 | name = Products;
124 | sourceTree = "";
125 | };
126 | 09B25ACF24B0C4A4004DFBA0 /* iOS */ = {
127 | isa = PBXGroup;
128 | children = (
129 | 09B25AD024B0C4A4004DFBA0 /* Info.plist */,
130 | );
131 | path = iOS;
132 | sourceTree = "";
133 | };
134 | 09B25AD624B0C4A4004DFBA0 /* macOS */ = {
135 | isa = PBXGroup;
136 | children = (
137 | 09B25AD724B0C4A4004DFBA0 /* Info.plist */,
138 | 09B25AD824B0C4A4004DFBA0 /* macOS.entitlements */,
139 | );
140 | path = macOS;
141 | sourceTree = "";
142 | };
143 | 09B25AE024B0C4A4004DFBA0 /* Tests iOS */ = {
144 | isa = PBXGroup;
145 | children = (
146 | 09B25AE124B0C4A4004DFBA0 /* Tests_iOS.swift */,
147 | 09B25AE324B0C4A4004DFBA0 /* Info.plist */,
148 | );
149 | path = "Tests iOS";
150 | sourceTree = "";
151 | };
152 | 09B25AEB24B0C4A4004DFBA0 /* Tests macOS */ = {
153 | isa = PBXGroup;
154 | children = (
155 | 09B25AEC24B0C4A4004DFBA0 /* Tests_macOS.swift */,
156 | 09B25AEE24B0C4A4004DFBA0 /* Info.plist */,
157 | );
158 | path = "Tests macOS";
159 | sourceTree = "";
160 | };
161 | 849CD2AE24BF72580096E6E5 /* Frameworks */ = {
162 | isa = PBXGroup;
163 | children = (
164 | );
165 | name = Frameworks;
166 | sourceTree = "";
167 | };
168 | /* End PBXGroup section */
169 |
170 | /* Begin PBXNativeTarget section */
171 | 09B25ACC24B0C4A4004DFBA0 /* iOS */ = {
172 | isa = PBXNativeTarget;
173 | buildConfigurationList = 09B25AF724B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "iOS" */;
174 | buildPhases = (
175 | 09B25AC924B0C4A4004DFBA0 /* Sources */,
176 | 09B25ACA24B0C4A4004DFBA0 /* Frameworks */,
177 | 09B25ACB24B0C4A4004DFBA0 /* Resources */,
178 | );
179 | buildRules = (
180 | );
181 | dependencies = (
182 | 849CD2AB24BF724C0096E6E5 /* PBXTargetDependency */,
183 | );
184 | name = iOS;
185 | packageProductDependencies = (
186 | 84E895AD24F10FBA000A82E6 /* WeatherCore */,
187 | );
188 | productName = iOS;
189 | productReference = 09B25ACD24B0C4A4004DFBA0 /* SwiftWeather.app */;
190 | productType = "com.apple.product-type.application";
191 | };
192 | 09B25AD424B0C4A4004DFBA0 /* macOS */ = {
193 | isa = PBXNativeTarget;
194 | buildConfigurationList = 09B25AFA24B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "macOS" */;
195 | buildPhases = (
196 | 09B25AD124B0C4A4004DFBA0 /* Sources */,
197 | 09B25AD224B0C4A4004DFBA0 /* Frameworks */,
198 | 09B25AD324B0C4A4004DFBA0 /* Resources */,
199 | );
200 | buildRules = (
201 | );
202 | dependencies = (
203 | 84E895AA24F10FA8000A82E6 /* PBXTargetDependency */,
204 | );
205 | name = macOS;
206 | packageProductDependencies = (
207 | 84E895AB24F10FB0000A82E6 /* WeatherCore */,
208 | );
209 | productName = macOS;
210 | productReference = 09B25AD524B0C4A4004DFBA0 /* SwiftWeather.app */;
211 | productType = "com.apple.product-type.application";
212 | };
213 | 09B25ADC24B0C4A4004DFBA0 /* Tests iOS */ = {
214 | isa = PBXNativeTarget;
215 | buildConfigurationList = 09B25AFD24B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "Tests iOS" */;
216 | buildPhases = (
217 | 09B25AD924B0C4A4004DFBA0 /* Sources */,
218 | 09B25ADA24B0C4A4004DFBA0 /* Frameworks */,
219 | 09B25ADB24B0C4A4004DFBA0 /* Resources */,
220 | );
221 | buildRules = (
222 | );
223 | dependencies = (
224 | 09B25ADF24B0C4A4004DFBA0 /* PBXTargetDependency */,
225 | );
226 | name = "Tests iOS";
227 | productName = "Tests iOS";
228 | productReference = 09B25ADD24B0C4A4004DFBA0 /* Tests iOS.xctest */;
229 | productType = "com.apple.product-type.bundle.ui-testing";
230 | };
231 | 09B25AE724B0C4A4004DFBA0 /* Tests macOS */ = {
232 | isa = PBXNativeTarget;
233 | buildConfigurationList = 09B25B0024B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "Tests macOS" */;
234 | buildPhases = (
235 | 09B25AE424B0C4A4004DFBA0 /* Sources */,
236 | 09B25AE524B0C4A4004DFBA0 /* Frameworks */,
237 | 09B25AE624B0C4A4004DFBA0 /* Resources */,
238 | );
239 | buildRules = (
240 | );
241 | dependencies = (
242 | 09B25AEA24B0C4A4004DFBA0 /* PBXTargetDependency */,
243 | );
244 | name = "Tests macOS";
245 | productName = "Tests macOS";
246 | productReference = 09B25AE824B0C4A4004DFBA0 /* Tests macOS.xctest */;
247 | productType = "com.apple.product-type.bundle.ui-testing";
248 | };
249 | /* End PBXNativeTarget section */
250 |
251 | /* Begin PBXProject section */
252 | 09B25AC124B0C4A3004DFBA0 /* Project object */ = {
253 | isa = PBXProject;
254 | attributes = {
255 | LastSwiftUpdateCheck = 1200;
256 | LastUpgradeCheck = 1200;
257 | TargetAttributes = {
258 | 09B25ACC24B0C4A4004DFBA0 = {
259 | CreatedOnToolsVersion = 12.0;
260 | };
261 | 09B25AD424B0C4A4004DFBA0 = {
262 | CreatedOnToolsVersion = 12.0;
263 | };
264 | 09B25ADC24B0C4A4004DFBA0 = {
265 | CreatedOnToolsVersion = 12.0;
266 | TestTargetID = 09B25ACC24B0C4A4004DFBA0;
267 | };
268 | 09B25AE724B0C4A4004DFBA0 = {
269 | CreatedOnToolsVersion = 12.0;
270 | TestTargetID = 09B25AD424B0C4A4004DFBA0;
271 | };
272 | };
273 | };
274 | buildConfigurationList = 09B25AC424B0C4A3004DFBA0 /* Build configuration list for PBXProject "SwiftWeather" */;
275 | compatibilityVersion = "Xcode 9.3";
276 | developmentRegion = en;
277 | hasScannedForEncodings = 0;
278 | knownRegions = (
279 | en,
280 | Base,
281 | );
282 | mainGroup = 09B25AC024B0C4A3004DFBA0;
283 | productRefGroup = 09B25ACE24B0C4A4004DFBA0 /* Products */;
284 | projectDirPath = "";
285 | projectRoot = "";
286 | targets = (
287 | 09B25ACC24B0C4A4004DFBA0 /* iOS */,
288 | 09B25AD424B0C4A4004DFBA0 /* macOS */,
289 | 09B25ADC24B0C4A4004DFBA0 /* Tests iOS */,
290 | 09B25AE724B0C4A4004DFBA0 /* Tests macOS */,
291 | );
292 | };
293 | /* End PBXProject section */
294 |
295 | /* Begin PBXResourcesBuildPhase section */
296 | 09B25ACB24B0C4A4004DFBA0 /* Resources */ = {
297 | isa = PBXResourcesBuildPhase;
298 | buildActionMask = 2147483647;
299 | files = (
300 | 09B25AF324B0C4A4004DFBA0 /* Assets.xcassets in Resources */,
301 | );
302 | runOnlyForDeploymentPostprocessing = 0;
303 | };
304 | 09B25AD324B0C4A4004DFBA0 /* Resources */ = {
305 | isa = PBXResourcesBuildPhase;
306 | buildActionMask = 2147483647;
307 | files = (
308 | 09B25AF424B0C4A4004DFBA0 /* Assets.xcassets in Resources */,
309 | );
310 | runOnlyForDeploymentPostprocessing = 0;
311 | };
312 | 09B25ADB24B0C4A4004DFBA0 /* Resources */ = {
313 | isa = PBXResourcesBuildPhase;
314 | buildActionMask = 2147483647;
315 | files = (
316 | );
317 | runOnlyForDeploymentPostprocessing = 0;
318 | };
319 | 09B25AE624B0C4A4004DFBA0 /* Resources */ = {
320 | isa = PBXResourcesBuildPhase;
321 | buildActionMask = 2147483647;
322 | files = (
323 | );
324 | runOnlyForDeploymentPostprocessing = 0;
325 | };
326 | /* End PBXResourcesBuildPhase section */
327 |
328 | /* Begin PBXSourcesBuildPhase section */
329 | 09B25AC924B0C4A4004DFBA0 /* Sources */ = {
330 | isa = PBXSourcesBuildPhase;
331 | buildActionMask = 2147483647;
332 | files = (
333 | 09B25AF124B0C4A4004DFBA0 /* ContentView.swift in Sources */,
334 | 09B25AEF24B0C4A4004DFBA0 /* SwiftWeatherApp.swift in Sources */,
335 | );
336 | runOnlyForDeploymentPostprocessing = 0;
337 | };
338 | 09B25AD124B0C4A4004DFBA0 /* Sources */ = {
339 | isa = PBXSourcesBuildPhase;
340 | buildActionMask = 2147483647;
341 | files = (
342 | 09B25AF224B0C4A4004DFBA0 /* ContentView.swift in Sources */,
343 | 09B25AF024B0C4A4004DFBA0 /* SwiftWeatherApp.swift in Sources */,
344 | );
345 | runOnlyForDeploymentPostprocessing = 0;
346 | };
347 | 09B25AD924B0C4A4004DFBA0 /* Sources */ = {
348 | isa = PBXSourcesBuildPhase;
349 | buildActionMask = 2147483647;
350 | files = (
351 | 09B25AE224B0C4A4004DFBA0 /* Tests_iOS.swift in Sources */,
352 | );
353 | runOnlyForDeploymentPostprocessing = 0;
354 | };
355 | 09B25AE424B0C4A4004DFBA0 /* Sources */ = {
356 | isa = PBXSourcesBuildPhase;
357 | buildActionMask = 2147483647;
358 | files = (
359 | 09B25AED24B0C4A4004DFBA0 /* Tests_macOS.swift in Sources */,
360 | );
361 | runOnlyForDeploymentPostprocessing = 0;
362 | };
363 | /* End PBXSourcesBuildPhase section */
364 |
365 | /* Begin PBXTargetDependency section */
366 | 09B25ADF24B0C4A4004DFBA0 /* PBXTargetDependency */ = {
367 | isa = PBXTargetDependency;
368 | target = 09B25ACC24B0C4A4004DFBA0 /* iOS */;
369 | targetProxy = 09B25ADE24B0C4A4004DFBA0 /* PBXContainerItemProxy */;
370 | };
371 | 09B25AEA24B0C4A4004DFBA0 /* PBXTargetDependency */ = {
372 | isa = PBXTargetDependency;
373 | target = 09B25AD424B0C4A4004DFBA0 /* macOS */;
374 | targetProxy = 09B25AE924B0C4A4004DFBA0 /* PBXContainerItemProxy */;
375 | };
376 | 849CD2AB24BF724C0096E6E5 /* PBXTargetDependency */ = {
377 | isa = PBXTargetDependency;
378 | productRef = 849CD2AA24BF724C0096E6E5 /* WeatherCore */;
379 | };
380 | 84E895AA24F10FA8000A82E6 /* PBXTargetDependency */ = {
381 | isa = PBXTargetDependency;
382 | productRef = 84E895A924F10FA8000A82E6 /* WeatherCore */;
383 | };
384 | /* End PBXTargetDependency section */
385 |
386 | /* Begin XCBuildConfiguration section */
387 | 09B25AF524B0C4A4004DFBA0 /* Debug */ = {
388 | isa = XCBuildConfiguration;
389 | buildSettings = {
390 | ALWAYS_SEARCH_USER_PATHS = NO;
391 | CLANG_ANALYZER_NONNULL = YES;
392 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
394 | CLANG_CXX_LIBRARY = "libc++";
395 | CLANG_ENABLE_MODULES = YES;
396 | CLANG_ENABLE_OBJC_ARC = YES;
397 | CLANG_ENABLE_OBJC_WEAK = YES;
398 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
399 | CLANG_WARN_BOOL_CONVERSION = YES;
400 | CLANG_WARN_COMMA = YES;
401 | CLANG_WARN_CONSTANT_CONVERSION = YES;
402 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
403 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
404 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
405 | CLANG_WARN_EMPTY_BODY = YES;
406 | CLANG_WARN_ENUM_CONVERSION = YES;
407 | CLANG_WARN_INFINITE_RECURSION = YES;
408 | CLANG_WARN_INT_CONVERSION = YES;
409 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
410 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
411 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
412 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
413 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
414 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
415 | CLANG_WARN_STRICT_PROTOTYPES = YES;
416 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
417 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
418 | CLANG_WARN_UNREACHABLE_CODE = YES;
419 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
420 | COPY_PHASE_STRIP = NO;
421 | DEBUG_INFORMATION_FORMAT = dwarf;
422 | ENABLE_STRICT_OBJC_MSGSEND = YES;
423 | ENABLE_TESTABILITY = YES;
424 | GCC_C_LANGUAGE_STANDARD = gnu11;
425 | GCC_DYNAMIC_NO_PIC = NO;
426 | GCC_NO_COMMON_BLOCKS = YES;
427 | GCC_OPTIMIZATION_LEVEL = 0;
428 | GCC_PREPROCESSOR_DEFINITIONS = (
429 | "DEBUG=1",
430 | "$(inherited)",
431 | );
432 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
433 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
434 | GCC_WARN_UNDECLARED_SELECTOR = YES;
435 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
436 | GCC_WARN_UNUSED_FUNCTION = YES;
437 | GCC_WARN_UNUSED_VARIABLE = YES;
438 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
439 | MTL_FAST_MATH = YES;
440 | ONLY_ACTIVE_ARCH = YES;
441 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
442 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
443 | };
444 | name = Debug;
445 | };
446 | 09B25AF624B0C4A4004DFBA0 /* Release */ = {
447 | isa = XCBuildConfiguration;
448 | buildSettings = {
449 | ALWAYS_SEARCH_USER_PATHS = NO;
450 | CLANG_ANALYZER_NONNULL = YES;
451 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
452 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
453 | CLANG_CXX_LIBRARY = "libc++";
454 | CLANG_ENABLE_MODULES = YES;
455 | CLANG_ENABLE_OBJC_ARC = YES;
456 | CLANG_ENABLE_OBJC_WEAK = YES;
457 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
458 | CLANG_WARN_BOOL_CONVERSION = YES;
459 | CLANG_WARN_COMMA = YES;
460 | CLANG_WARN_CONSTANT_CONVERSION = YES;
461 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
462 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
463 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
464 | CLANG_WARN_EMPTY_BODY = YES;
465 | CLANG_WARN_ENUM_CONVERSION = YES;
466 | CLANG_WARN_INFINITE_RECURSION = YES;
467 | CLANG_WARN_INT_CONVERSION = YES;
468 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
469 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
470 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
471 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
472 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
473 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
474 | CLANG_WARN_STRICT_PROTOTYPES = YES;
475 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
476 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
477 | CLANG_WARN_UNREACHABLE_CODE = YES;
478 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
479 | COPY_PHASE_STRIP = NO;
480 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
481 | ENABLE_NS_ASSERTIONS = NO;
482 | ENABLE_STRICT_OBJC_MSGSEND = YES;
483 | GCC_C_LANGUAGE_STANDARD = gnu11;
484 | GCC_NO_COMMON_BLOCKS = YES;
485 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
486 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
487 | GCC_WARN_UNDECLARED_SELECTOR = YES;
488 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
489 | GCC_WARN_UNUSED_FUNCTION = YES;
490 | GCC_WARN_UNUSED_VARIABLE = YES;
491 | MTL_ENABLE_DEBUG_INFO = NO;
492 | MTL_FAST_MATH = YES;
493 | SWIFT_COMPILATION_MODE = wholemodule;
494 | SWIFT_OPTIMIZATION_LEVEL = "-O";
495 | };
496 | name = Release;
497 | };
498 | 09B25AF824B0C4A4004DFBA0 /* Debug */ = {
499 | isa = XCBuildConfiguration;
500 | buildSettings = {
501 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
502 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
503 | CODE_SIGN_STYLE = Automatic;
504 | ENABLE_PREVIEWS = YES;
505 | INFOPLIST_FILE = iOS/Info.plist;
506 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
507 | LD_RUNPATH_SEARCH_PATHS = (
508 | "$(inherited)",
509 | "@executable_path/Frameworks",
510 | );
511 | PRODUCT_BUNDLE_IDENTIFIER = readdle.Weather.SwiftWeather;
512 | PRODUCT_NAME = SwiftWeather;
513 | SDKROOT = iphoneos;
514 | SWIFT_VERSION = 5.0;
515 | TARGETED_DEVICE_FAMILY = "1,2";
516 | };
517 | name = Debug;
518 | };
519 | 09B25AF924B0C4A4004DFBA0 /* Release */ = {
520 | isa = XCBuildConfiguration;
521 | buildSettings = {
522 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
523 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
524 | CODE_SIGN_STYLE = Automatic;
525 | ENABLE_PREVIEWS = YES;
526 | INFOPLIST_FILE = iOS/Info.plist;
527 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
528 | LD_RUNPATH_SEARCH_PATHS = (
529 | "$(inherited)",
530 | "@executable_path/Frameworks",
531 | );
532 | PRODUCT_BUNDLE_IDENTIFIER = readdle.Weather.SwiftWeather;
533 | PRODUCT_NAME = SwiftWeather;
534 | SDKROOT = iphoneos;
535 | SWIFT_VERSION = 5.0;
536 | TARGETED_DEVICE_FAMILY = "1,2";
537 | VALIDATE_PRODUCT = YES;
538 | };
539 | name = Release;
540 | };
541 | 09B25AFB24B0C4A4004DFBA0 /* Debug */ = {
542 | isa = XCBuildConfiguration;
543 | buildSettings = {
544 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
545 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
546 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
547 | CODE_SIGN_IDENTITY = "-";
548 | CODE_SIGN_STYLE = Automatic;
549 | COMBINE_HIDPI_IMAGES = YES;
550 | ENABLE_PREVIEWS = YES;
551 | INFOPLIST_FILE = macOS/Info.plist;
552 | LD_RUNPATH_SEARCH_PATHS = (
553 | "$(inherited)",
554 | "@executable_path/../Frameworks",
555 | );
556 | MACOSX_DEPLOYMENT_TARGET = 10.16;
557 | PRODUCT_BUNDLE_IDENTIFIER = readdle.Weather.SwiftWeather;
558 | PRODUCT_NAME = SwiftWeather;
559 | SDKROOT = macosx;
560 | SWIFT_VERSION = 5.0;
561 | };
562 | name = Debug;
563 | };
564 | 09B25AFC24B0C4A4004DFBA0 /* Release */ = {
565 | isa = XCBuildConfiguration;
566 | buildSettings = {
567 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
568 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
569 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
570 | CODE_SIGN_IDENTITY = "-";
571 | CODE_SIGN_STYLE = Automatic;
572 | COMBINE_HIDPI_IMAGES = YES;
573 | ENABLE_PREVIEWS = YES;
574 | INFOPLIST_FILE = macOS/Info.plist;
575 | LD_RUNPATH_SEARCH_PATHS = (
576 | "$(inherited)",
577 | "@executable_path/../Frameworks",
578 | );
579 | MACOSX_DEPLOYMENT_TARGET = 10.16;
580 | PRODUCT_BUNDLE_IDENTIFIER = readdle.Weather.SwiftWeather;
581 | PRODUCT_NAME = SwiftWeather;
582 | SDKROOT = macosx;
583 | SWIFT_VERSION = 5.0;
584 | };
585 | name = Release;
586 | };
587 | 09B25AFE24B0C4A4004DFBA0 /* Debug */ = {
588 | isa = XCBuildConfiguration;
589 | buildSettings = {
590 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
591 | CODE_SIGN_STYLE = Automatic;
592 | INFOPLIST_FILE = "Tests iOS/Info.plist";
593 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
594 | LD_RUNPATH_SEARCH_PATHS = (
595 | "$(inherited)",
596 | "@executable_path/Frameworks",
597 | "@loader_path/Frameworks",
598 | );
599 | PRODUCT_BUNDLE_IDENTIFIER = "readdle.Weather.Tests-iOS";
600 | PRODUCT_NAME = "$(TARGET_NAME)";
601 | SDKROOT = iphoneos;
602 | SWIFT_VERSION = 5.0;
603 | TARGETED_DEVICE_FAMILY = "1,2";
604 | TEST_TARGET_NAME = iOS;
605 | };
606 | name = Debug;
607 | };
608 | 09B25AFF24B0C4A4004DFBA0 /* Release */ = {
609 | isa = XCBuildConfiguration;
610 | buildSettings = {
611 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
612 | CODE_SIGN_STYLE = Automatic;
613 | INFOPLIST_FILE = "Tests iOS/Info.plist";
614 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
615 | LD_RUNPATH_SEARCH_PATHS = (
616 | "$(inherited)",
617 | "@executable_path/Frameworks",
618 | "@loader_path/Frameworks",
619 | );
620 | PRODUCT_BUNDLE_IDENTIFIER = "readdle.Weather.Tests-iOS";
621 | PRODUCT_NAME = "$(TARGET_NAME)";
622 | SDKROOT = iphoneos;
623 | SWIFT_VERSION = 5.0;
624 | TARGETED_DEVICE_FAMILY = "1,2";
625 | TEST_TARGET_NAME = iOS;
626 | VALIDATE_PRODUCT = YES;
627 | };
628 | name = Release;
629 | };
630 | 09B25B0124B0C4A4004DFBA0 /* Debug */ = {
631 | isa = XCBuildConfiguration;
632 | buildSettings = {
633 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
634 | CODE_SIGN_STYLE = Automatic;
635 | COMBINE_HIDPI_IMAGES = YES;
636 | INFOPLIST_FILE = "Tests macOS/Info.plist";
637 | LD_RUNPATH_SEARCH_PATHS = (
638 | "$(inherited)",
639 | "@executable_path/../Frameworks",
640 | "@loader_path/../Frameworks",
641 | );
642 | MACOSX_DEPLOYMENT_TARGET = 10.16;
643 | PRODUCT_BUNDLE_IDENTIFIER = "readdle.Weather.Tests-macOS";
644 | PRODUCT_NAME = "$(TARGET_NAME)";
645 | SDKROOT = macosx;
646 | SWIFT_VERSION = 5.0;
647 | TEST_TARGET_NAME = macOS;
648 | };
649 | name = Debug;
650 | };
651 | 09B25B0224B0C4A4004DFBA0 /* Release */ = {
652 | isa = XCBuildConfiguration;
653 | buildSettings = {
654 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
655 | CODE_SIGN_STYLE = Automatic;
656 | COMBINE_HIDPI_IMAGES = YES;
657 | INFOPLIST_FILE = "Tests macOS/Info.plist";
658 | LD_RUNPATH_SEARCH_PATHS = (
659 | "$(inherited)",
660 | "@executable_path/../Frameworks",
661 | "@loader_path/../Frameworks",
662 | );
663 | MACOSX_DEPLOYMENT_TARGET = 10.16;
664 | PRODUCT_BUNDLE_IDENTIFIER = "readdle.Weather.Tests-macOS";
665 | PRODUCT_NAME = "$(TARGET_NAME)";
666 | SDKROOT = macosx;
667 | SWIFT_VERSION = 5.0;
668 | TEST_TARGET_NAME = macOS;
669 | };
670 | name = Release;
671 | };
672 | /* End XCBuildConfiguration section */
673 |
674 | /* Begin XCConfigurationList section */
675 | 09B25AC424B0C4A3004DFBA0 /* Build configuration list for PBXProject "SwiftWeather" */ = {
676 | isa = XCConfigurationList;
677 | buildConfigurations = (
678 | 09B25AF524B0C4A4004DFBA0 /* Debug */,
679 | 09B25AF624B0C4A4004DFBA0 /* Release */,
680 | );
681 | defaultConfigurationIsVisible = 0;
682 | defaultConfigurationName = Release;
683 | };
684 | 09B25AF724B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "iOS" */ = {
685 | isa = XCConfigurationList;
686 | buildConfigurations = (
687 | 09B25AF824B0C4A4004DFBA0 /* Debug */,
688 | 09B25AF924B0C4A4004DFBA0 /* Release */,
689 | );
690 | defaultConfigurationIsVisible = 0;
691 | defaultConfigurationName = Release;
692 | };
693 | 09B25AFA24B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "macOS" */ = {
694 | isa = XCConfigurationList;
695 | buildConfigurations = (
696 | 09B25AFB24B0C4A4004DFBA0 /* Debug */,
697 | 09B25AFC24B0C4A4004DFBA0 /* Release */,
698 | );
699 | defaultConfigurationIsVisible = 0;
700 | defaultConfigurationName = Release;
701 | };
702 | 09B25AFD24B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "Tests iOS" */ = {
703 | isa = XCConfigurationList;
704 | buildConfigurations = (
705 | 09B25AFE24B0C4A4004DFBA0 /* Debug */,
706 | 09B25AFF24B0C4A4004DFBA0 /* Release */,
707 | );
708 | defaultConfigurationIsVisible = 0;
709 | defaultConfigurationName = Release;
710 | };
711 | 09B25B0024B0C4A4004DFBA0 /* Build configuration list for PBXNativeTarget "Tests macOS" */ = {
712 | isa = XCConfigurationList;
713 | buildConfigurations = (
714 | 09B25B0124B0C4A4004DFBA0 /* Debug */,
715 | 09B25B0224B0C4A4004DFBA0 /* Release */,
716 | );
717 | defaultConfigurationIsVisible = 0;
718 | defaultConfigurationName = Release;
719 | };
720 | /* End XCConfigurationList section */
721 |
722 | /* Begin XCSwiftPackageProductDependency section */
723 | 849CD2AA24BF724C0096E6E5 /* WeatherCore */ = {
724 | isa = XCSwiftPackageProductDependency;
725 | productName = WeatherCore;
726 | };
727 | 84E895A924F10FA8000A82E6 /* WeatherCore */ = {
728 | isa = XCSwiftPackageProductDependency;
729 | productName = WeatherCore;
730 | };
731 | 84E895AB24F10FB0000A82E6 /* WeatherCore */ = {
732 | isa = XCSwiftPackageProductDependency;
733 | productName = WeatherCore;
734 | };
735 | 84E895AD24F10FBA000A82E6 /* WeatherCore */ = {
736 | isa = XCSwiftPackageProductDependency;
737 | productName = WeatherCore;
738 | };
739 | /* End XCSwiftPackageProductDependency section */
740 | };
741 | rootObject = 09B25AC124B0C4A3004DFBA0 /* Project object */;
742 | }
743 |
--------------------------------------------------------------------------------