├── 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 | 4 | 5 | 11 | 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 ![AndroidCore](https://github.com/andriydruk/swift-weather-app/workflows/AndroidCore/badge.svg) ![MacCore](https://github.com/andriydruk/swift-weather-app/workflows/MacCore/badge.svg) 11 | Cross-platform Swift application 12 | 13 | ![GitHub Logo](./doc/device-2020-04-21-000209.png) 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 | --------------------------------------------------------------------------------