├── .circleci └── config.yml ├── .gitignore ├── .hound.yml ├── .ruby-version ├── .swiftformat ├── .swiftlint.yml ├── Brewfile ├── CONTRIBUTING.md ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── Config └── Secrets-Example.h ├── Docs ├── .gitkeep └── PrivacyPolicy.md ├── Frameworks └── AppCenter │ ├── AppCenter.framework │ ├── AppCenter │ ├── Headers │ │ ├── AppCenter.h │ │ ├── MSAbstractLog.h │ │ ├── MSAppCenter.h │ │ ├── MSAppCenterErrors.h │ │ ├── MSChannelGroupProtocol.h │ │ ├── MSChannelProtocol.h │ │ ├── MSConstants.h │ │ ├── MSCustomProperties.h │ │ ├── MSDevice.h │ │ ├── MSEnable.h │ │ ├── MSLog.h │ │ ├── MSLogWithProperties.h │ │ ├── MSLogger.h │ │ ├── MSService.h │ │ ├── MSServiceAbstract.h │ │ ├── MSWrapperLogger.h │ │ └── MSWrapperSdk.h │ └── Modules │ │ └── module.modulemap │ └── AppCenterCrashes.framework │ ├── AppCenterCrashes │ ├── Headers │ ├── AppCenterCrashes.h │ ├── MSAbstractLog.h │ ├── MSCrashHandlerSetupDelegate.h │ ├── MSCrashes.h │ ├── MSCrashesDelegate.h │ ├── MSErrorAttachmentLog+Utility.h │ ├── MSErrorAttachmentLog.h │ ├── MSErrorReport.h │ ├── MSService.h │ ├── MSServiceAbstract.h │ └── MSWrapperCrashesHelper.h │ └── Modules │ └── module.modulemap ├── LICENSE ├── README.md ├── Sources ├── Tropos │ ├── Controllers │ │ ├── .gitkeep │ │ ├── AcknowledgementsParser.swift │ │ ├── AppDelegate.swift │ │ ├── AppearanceController.swift │ │ ├── TRAnalyticsController.h │ │ ├── TRAnalyticsController.m │ │ ├── TRApplicationController.h │ │ ├── TRApplicationController.m │ │ ├── TRWeatherController.h │ │ └── TRWeatherController.m │ ├── Extensions │ │ ├── .gitkeep │ │ ├── CATransition.swift │ │ ├── INInteraction.swift │ │ ├── NSBundle+TRBundleInfo.h │ │ ├── NSBundle+TRBundleInfo.m │ │ ├── NSError+TRErrors.h │ │ ├── NSError+TRErrors.m │ │ ├── NSNumber+TRRoundedNumber.h │ │ ├── NSNumber+TRRoundedNumber.m │ │ ├── RACSignal+TROperators.h │ │ ├── RACSignal+TROperators.m │ │ ├── TRSettingsController+TRObservation.h │ │ ├── TRSettingsController+TRObservation.m │ │ ├── TRWeatherUpdate+Analytics.h │ │ ├── TRWeatherUpdate+Analytics.m │ │ ├── UIApplication+TRReactiveBackgroundTask.h │ │ ├── UIApplication+TRReactiveBackgroundTask.m │ │ ├── UIImage+TRColorBackdrop.h │ │ ├── UIImage+TRColorBackdrop.m │ │ ├── UIScrollView+TRReactiveCocoa.h │ │ └── UIScrollView+TRReactiveCocoa.m │ ├── Headers │ │ ├── .gitkeep │ │ └── Tropos-Bridging-Header.h │ ├── Info.plist │ ├── Models │ │ ├── .gitkeep │ │ └── Color.swift │ ├── Protocols │ │ ├── .gitkeep │ │ ├── TRAnalyticsEvent.h │ │ └── TRApplication.h │ ├── Resources │ │ ├── .gitkeep │ │ ├── Fonts │ │ │ ├── DINNextLTPro-Light.otf │ │ │ └── DINNextLTPro-Regular.otf │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── 1024 App Icon for the App Store.png │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-167.png │ │ │ │ ├── Icon-20.png │ │ │ │ ├── Icon-60.png │ │ │ │ ├── Icon-60@2x.png │ │ │ │ ├── Icon-60@3x.png │ │ │ │ ├── Icon-72.png │ │ │ │ ├── Icon-72@2x.png │ │ │ │ ├── Icon-76.png │ │ │ │ ├── Icon-76@2x.png │ │ │ │ ├── Icon-Small-1.png │ │ │ │ ├── Icon-Small-40.png │ │ │ │ ├── Icon-Small-40@2x-1.png │ │ │ │ ├── Icon-Small-40@2x.png │ │ │ │ ├── Icon-Small-40@3x.png │ │ │ │ ├── Icon-Small-42.png │ │ │ │ ├── Icon-Small-43.png │ │ │ │ ├── Icon-Small-50.png │ │ │ │ ├── Icon-Small-50@2x.png │ │ │ │ ├── Icon-Small.png │ │ │ │ ├── Icon-Small@2x-1.png │ │ │ │ ├── Icon-Small@2x.png │ │ │ │ ├── Icon-Small@3x.png │ │ │ │ ├── Icon.png │ │ │ │ └── Icon@2x.png │ │ │ ├── Contents.json │ │ │ ├── settings.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── settings.pdf │ │ │ ├── temp.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── temp.pdf │ │ │ ├── thoughtbot-logo-full.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── thoughtbot-logo-full.pdf │ │ │ └── wind-icon-small.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── wind-icon-small.pdf │ │ ├── Nibs │ │ │ ├── .gitkeep │ │ │ ├── CWForecastDetailView.xib │ │ │ ├── LaunchScreen.xib │ │ │ └── TRDailyForecastView.xib │ │ ├── Other-Sources │ │ │ └── .gitkeep │ │ ├── Storyboards │ │ │ ├── .gitkeep │ │ │ └── Base.lproj │ │ │ │ └── Main.storyboard │ │ ├── en.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ ├── it.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ ├── pl-PL.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ └── sv.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ ├── Tropos.entitlements │ ├── ViewControllers │ │ ├── SettingsTableViewController.swift │ │ ├── TRWeatherViewController.h │ │ ├── TRWeatherViewController.m │ │ ├── TextViewController.swift │ │ └── WebViewController.swift │ └── Views │ │ ├── .gitkeep │ │ ├── FadingImageView.swift │ │ ├── FadingLabel.swift │ │ ├── TRCircularProgressLayer.h │ │ ├── TRCircularProgressLayer.m │ │ ├── TRColorBackdropLayer.h │ │ ├── TRColorBackdropLayer.m │ │ ├── TRDailyForecastView.h │ │ ├── TRDailyForecastView.m │ │ ├── TRNavigationBar.h │ │ ├── TRNavigationBar.m │ │ ├── TRRefreshControl.h │ │ ├── TRRefreshControl.m │ │ ├── TRRefreshLayer.h │ │ ├── TRRefreshLayer.m │ │ ├── TRRefreshView.h │ │ ├── TRRefreshView.m │ │ └── TRTableViewCell.swift ├── TroposCore │ ├── .gitkeep │ ├── Controllers │ │ ├── ForecastController.swift │ │ ├── GeocodeController.swift │ │ ├── LocationController.swift │ │ └── SettingsController.swift │ ├── Extensions │ │ ├── CLLocation+TRRecentLocation.swift │ │ ├── Date+Relative.swift │ │ ├── ForecastController+Bridging.swift │ │ ├── NSAttributedString.swift │ │ ├── NSBundle.swift │ │ ├── NSFileCoordinator.swift │ │ ├── NSLocalizedString.swift │ │ ├── UIColor.swift │ │ └── UIFont.swift │ ├── Formatters │ │ ├── PrecipitationChanceFormatter.swift │ │ ├── RelativeDateFormatter.swift │ │ ├── TemperatureComparisonFormatter.swift │ │ ├── TemperatureFormatter.swift │ │ └── WindSpeedFormatter.swift │ ├── Info.plist │ ├── Models │ │ ├── CardinalDirection.swift │ │ ├── DailyForecast.swift │ │ ├── Precipitation.swift │ │ ├── PrecipitationChance.swift │ │ ├── Temperature.swift │ │ ├── TimeOfDay.swift │ │ ├── UnitSystem.swift │ │ ├── WeatherUpdate.swift │ │ └── WeatherUpdateCache.swift │ ├── Protocols │ │ └── Geocoder.swift │ ├── Resources │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── Weather Icons │ │ │ │ ├── Contents.json │ │ │ │ ├── clear-day.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── clear-day.pdf │ │ │ │ ├── clear-night.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── clear-night.pdf │ │ │ │ ├── cloudy.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── cloudy.pdf │ │ │ │ ├── fog.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── fog.pdf │ │ │ │ ├── partly-cloudy-day.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── partly-cloudy-day.pdf │ │ │ │ ├── partly-cloudy-night.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── partly-cloudy-night.pdf │ │ │ │ ├── rain.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── rain.pdf │ │ │ │ ├── sleet.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── sleet.pdf │ │ │ │ ├── snow.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── snow.pdf │ │ │ │ └── wind.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── wind.pdf │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ ├── it.lproj │ │ │ └── Localizable.strings │ │ ├── pl-PL.lproj │ │ │ └── Localizable.strings │ │ └── sv.lproj │ │ │ └── Localizable.strings │ ├── TRErrors.h │ ├── TroposCore.h │ ├── TroposCore.m │ └── ViewModels │ │ ├── .gitkeep │ │ ├── DailyForecastViewModel.swift │ │ └── WeatherViewModel.swift └── TroposIntents │ ├── Base.lproj │ └── Intents.intentdefinition │ ├── CheckWeatherIntentHandler.swift │ ├── Info.plist │ ├── IntentHandler.swift │ ├── TroposIntents-Bridging-Header.h │ ├── TroposIntents.entitlements │ ├── it.lproj │ └── Intents.strings │ ├── pl-PL.lproj │ └── Intents.strings │ └── sv.lproj │ └── Intents.strings ├── Tests ├── TroposCoreTests │ ├── .gitkeep │ ├── Controllers │ │ ├── ForecastControllerSpec.swift │ │ ├── GeocodeControllerSpec.swift │ │ └── SettingsControllerSpec.swift │ ├── Extensions │ │ └── TRTemperatureColorsSpec.swift │ ├── Formatters │ │ ├── TemperatureComparisonFormatterSpec.swift │ │ └── TemperatureFormatterSpec.swift │ ├── Info.plist │ ├── Models │ │ ├── CardinalDirectionSpec.swift │ │ ├── PrecipitationSpec.swift │ │ ├── TemperatureSpec.swift │ │ ├── WeatherUpdateCacheSpec.swift │ │ └── WeatherUpdateSpec.swift │ ├── Resources │ │ └── New York.placemark │ ├── Support │ │ ├── CoreLocation.swift │ │ ├── NSDate+ISO8601.swift │ │ ├── OHHTTPStubs+Matchers.swift │ │ ├── StubCondition.swift │ │ ├── TestError.swift │ │ ├── TestGeocoder.swift │ │ ├── TestPlacemark.swift │ │ └── UserDefaults+Random.swift │ └── ViewModels │ │ └── DailyForecastViewModelSpec.swift └── TroposTests │ ├── .gitkeep │ ├── Controllers │ └── ApplicationControllerSpec.swift │ ├── Info.plist │ └── Support │ ├── .gitkeep │ ├── TestApplication.swift │ ├── TestLocationController.swift │ └── TroposTests-Bridging-Header.h ├── Tropos.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Tropos (San Francisco).xcscheme │ ├── Tropos.xcscheme │ └── TroposCore.xcscheme ├── bin ├── archive ├── setup ├── swiftlint ├── test └── update └── fastlane ├── Appfile ├── Fastfile ├── Gymfile ├── Matchfile └── README.md /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | xcode: 4 | macos: 5 | xcode: "10.1.0" 6 | 7 | jobs: 8 | swiftformat: 9 | executor: xcode 10 | steps: 11 | - checkout 12 | - run: brew update && brew install swiftformat 13 | - run: swiftformat --version 14 | - run: swiftformat --lint . 15 | 16 | build-and-test: 17 | executor: xcode 18 | 19 | environment: 20 | CARTHAGE_LOG_PATH: log/carthage-build.log 21 | GEM_INSTALL_LOG_PATH: log/gem-install.log 22 | 23 | steps: 24 | - checkout 25 | - run: mkdir log 26 | - run: 27 | name: Save Xcode version 28 | command: xcodebuild -version | tee .circle-xcode-version 29 | - run: 30 | name: Save Carthage cache version 31 | command: echo "$CARTHAGE_CACHE_VERSION" | tee .circle-carthage-cache-version 32 | 33 | - restore_cache: 34 | keys: 35 | - carthage-{{ checksum ".circle-carthage-cache-version" }}-{{ checksum ".circle-xcode-version" }}-{{ checksum "Cartfile.resolved" }} 36 | - carthage-{{ checksum ".circle-carthage-cache-version" }}-{{ checksum ".circle-xcode-version" }} 37 | - carthage-{{ checksum ".circle-carthage-cache-version" }}- 38 | 39 | - run: bin/setup 40 | 41 | - save_cache: 42 | key: carthage-{{ checksum ".circle-carthage-cache-version" }}-{{ checksum ".circle-xcode-version" }}-{{ checksum "Cartfile.resolved" }} 43 | paths: 44 | - "~/Library/Caches/org.carthage.CarthageKit" 45 | - "Carthage" 46 | 47 | - run: bin/test 48 | - run: bin/archive 49 | 50 | - store_artifacts: 51 | path: log 52 | 53 | workflows: 54 | version: 2 55 | build-and-test: 56 | jobs: 57 | - build-and-test 58 | - swiftformat 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Carthage/Checkouts 2 | 3 | # OS X Finder 4 | .DS_Store 5 | 6 | # Xcode per-user config 7 | *.mode1 8 | *.mode1v3 9 | *.mode2v3 10 | *.perspective 11 | *.perspectivev3 12 | *.pbxuser 13 | xcuserdata 14 | *.xccheckout 15 | 16 | # Build products 17 | build/ 18 | *.o 19 | *.LinkFileList 20 | *.hmap 21 | 22 | # Log files 23 | /log/ 24 | .*.log 25 | 26 | # Automatic backup files 27 | *~.nib/ 28 | *.swp 29 | *~ 30 | *.dat 31 | *.dep 32 | 33 | # Cocoapods 34 | Pods 35 | 36 | # AppCode specific files 37 | .idea/ 38 | *.iml 39 | 40 | # fastlane 41 | fastlane/report.xml 42 | fastlane/Preview.html 43 | fastlane/screenshots 44 | fastlane/test_output 45 | 46 | Secrets.h 47 | .env 48 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | swiftlint: 2 | config_file: .swiftlint.yml 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.0 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --exclude Carthage 2 | --ifdef outdent 3 | --indent 4 4 | --self init-only 5 | --stripunusedargs closure-only 6 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - function_body_length 4 | identifier_name: 5 | excluded: 6 | - TroposCoreLocalizedString(_:comment:) 7 | min_length: 0 8 | trailing_comma: 9 | mandatory_comma: true 10 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | # vim: ft=ruby 2 | 3 | brew "carthage" 4 | brew "swiftformat" 5 | brew "swiftlint" 6 | cask "fastlane" 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love pull requests from everyone. Follow the thoughtbot [code of conduct] 2 | while contributing. 3 | 4 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 5 | 6 | ## Dependencies 7 | 8 | Tropos uses [CocoaPods][] for dependency management. Once you have it installed, 9 | simply run `bin/setup` to install the dependencies. 10 | 11 | [CocoaPods]: https://cocoapods.org/ 12 | 13 | ## Contributing 14 | 15 | 1. Fork the repo. 16 | 2. Run the tests. We only take pull requests with passing tests, and it's 17 | great to know that you have a clean slate. 18 | 3. Add a test for your change. Only refactoring and documentation changes 19 | require no new tests. If you are adding functionality or fixing a bug, we 20 | need a test! 21 | 4. Make the test pass. 22 | 5. Push to your fork and submit a pull request. 23 | 24 | At this point you're waiting on us. We like to at least comment on, if not 25 | accept, pull requests within three business days (and, typically, one business 26 | day). We may suggest some changes or improvements or alternatives. 27 | 28 | Some things that will increase the chance that your pull request is accepted, 29 | 30 | * Include tests that fail without your code, and pass with it 31 | * Update the documentation, the surrounding one, examples elsewhere, guides, 32 | whatever is affected by your contribution 33 | * Follow the existing style of the project 34 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveCocoa/ReactiveObjCBridge" 2 | github "ReactiveCocoa/ReactiveSwift" 3 | github "mixpanel/mixpanel-iphone" 4 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "AliSoftware/OHHTTPStubs" 2 | github "Quick/Quick" 3 | github "Quick/Nimble" 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "AliSoftware/OHHTTPStubs" "6.1.0" 2 | github "Quick/Nimble" "v8.0.1" 3 | github "Quick/Quick" "v1.3.0" 4 | github "ReactiveCocoa/ReactiveObjC" "3.1.0" 5 | github "ReactiveCocoa/ReactiveObjCBridge" "3.1.0" 6 | github "ReactiveCocoa/ReactiveSwift" "3.1.0" 7 | github "antitypical/Result" "3.2.4" 8 | github "mixpanel/mixpanel-iphone" "v3.3.7" 9 | -------------------------------------------------------------------------------- /Config/Secrets-Example.h: -------------------------------------------------------------------------------- 1 | @class NSString; 2 | 3 | static NSString *const TRAppCenterSecret = @""; 4 | static NSString *const TRForecastAPIKey = @""; 5 | static NSString *const TRMixpanelToken = @""; 6 | -------------------------------------------------------------------------------- /Docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Docs/.gitkeep -------------------------------------------------------------------------------- /Docs/PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # General Information 2 | 3 | We collect general application usage information from your mobile device. This includes crash reporting information and the locality and administrative area in which you use the application. The information we collect is used to improve the content of our application and is not shared with or sold to other organizations for commercial purposes, except to provide products or services you’ve requested, when we have your permission, or under the following circumstances: 4 | 5 | It is necessary to share information in order to investigate, prevent, or take action regarding illegal activities, suspected fraud, situations involving potential threats to the physical safety of any person, violations of Terms of Service, or as otherwise required by law. 6 | 7 | We transfer information about you if thoughtbot, inc. is acquired by or merged with another company. In this event, thoughtbot, inc. will notify you before information about you is transferred and becomes subject to a different privacy policy. 8 | 9 | # Information Gathering and Usage 10 | 11 | When you grant the application access to the location services provided by your mobile device and fetch weather conditions for your current location, we will collect your approximate location accurate to only 0.5 miles. 12 | 13 | thoughtbot, inc. uses collected information for the following general purposes: products and services provision, services improvement, and research. 14 | 15 | # Cookies 16 | 17 | We do not use cookies to store personal data on your mobile device. 18 | 19 | # Disclosure 20 | 21 | thoughtbot, inc. may disclose personally identifiable information under special circumstances, such as to comply with subpoenas or when your actions violate the Terms of Service. 22 | 23 | # Changes 24 | 25 | thoughtbot, inc. may periodically update this policy. We will notify you about significant changes in the way we treat personal information by placing a prominent notice on our site. 26 | 27 | # Questions 28 | 29 | Any questions about this Privacy Policy should be addressed to thoughtbot, inc. . -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/AppCenter: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Frameworks/AppCenter/AppCenter.framework/AppCenter -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/AppCenter.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSAbstractLog.h" 4 | #import "MSAppCenter.h" 5 | #import "MSAppCenterErrors.h" 6 | #import "MSChannelGroupProtocol.h" 7 | #import "MSChannelProtocol.h" 8 | #import "MSConstants.h" 9 | #import "MSDevice.h" 10 | #import "MSEnable.h" 11 | #import "MSLog.h" 12 | #import "MSLogWithProperties.h" 13 | #import "MSLogger.h" 14 | #import "MSService.h" 15 | #import "MSServiceAbstract.h" 16 | #import "MSWrapperLogger.h" 17 | #import "MSWrapperSdk.h" 18 | 19 | #if !TARGET_OS_TV 20 | #import "MSCustomProperties.h" 21 | #endif 22 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSAbstractLog.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MSAbstractLog : NSObject 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSAppCenterErrors.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | #pragma mark - Domain 6 | 7 | extern NSString *const kMSACErrorDomain; 8 | 9 | #pragma mark - Log 10 | 11 | // Error codes 12 | NS_ENUM(NSInteger){kMSACLogInvalidContainerErrorCode = 1}; 13 | 14 | // Error descriptions 15 | extern NSString const *kMSACLogInvalidContainerErrorDesc; 16 | 17 | #pragma mark - Connection 18 | 19 | // Error codes 20 | NS_ENUM(NSInteger){kMSACConnectionPausedErrorCode = 100, kMSACConnectionHttpErrorCode = 101}; 21 | 22 | // Error descriptions 23 | extern NSString const *kMSACConnectionHttpErrorDesc; 24 | extern NSString const *kMSACConnectionPausedErrorDesc; 25 | 26 | // Error user info keys 27 | extern NSString const *kMSACConnectionHttpCodeErrorKey; 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSChannelGroupProtocol.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSChannelProtocol.h" 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | @class MSChannelUnitConfiguration; 8 | 9 | @protocol MSIngestionProtocol; 10 | @protocol MSChannelUnitProtocol; 11 | 12 | /** 13 | * `MSChannelGroupProtocol` represents a kind of channel that contains constituent MSChannelUnit objects. When an operation from the 14 | * `MSChannelProtocol` is performed on the group, that operation should be propagated to its constituent MSChannelUnit objects. 15 | */ 16 | @protocol MSChannelGroupProtocol 17 | 18 | /** 19 | * Initialize a channel unit with the given configuration. 20 | * 21 | * @param configuration channel configuration. 22 | * 23 | * @return The added `MSChannelUnitProtocol`. Use this object to enqueue logs. 24 | */ 25 | - (id)addChannelUnitWithConfiguration:(MSChannelUnitConfiguration *)configuration; 26 | 27 | /** 28 | * Initialize a channel unit with the given configuration. 29 | * 30 | * @param configuration channel configuration. 31 | * @param ingestion The alternative ingestion object 32 | * 33 | * @return The added `MSChannelUnitProtocol`. Use this object to enqueue logs. 34 | */ 35 | - (id)addChannelUnitWithConfiguration:(MSChannelUnitConfiguration *)configuration 36 | withIngestion:(nullable id)ingestion; 37 | 38 | /** 39 | * Change the base URL (schema + authority + port only) used to communicate with the backend. 40 | * 41 | * @param logUrl base URL to use for backend communication. 42 | */ 43 | - (void)setLogUrl:(NSString *)logUrl; 44 | 45 | /** 46 | * Set the app secret. 47 | * 48 | * @param appSecret The app secret. 49 | */ 50 | - (void)setAppSecret:(NSString *)appSecret; 51 | 52 | /** 53 | * Set the maximum size of the internal storage. This method must be called before App Center is started. 54 | * 55 | * @discussion The default maximum database size is 10485760 bytes (10 MiB). 56 | * 57 | * @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size 58 | * (default is 4096 bytes). Values below 24576 bytes (24 KiB) will be ignored. 59 | * @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size 60 | * is successful, and `NO` otherwise. 61 | */ 62 | - (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler; 63 | 64 | /** 65 | * Return a channel unit instance for the given groupId. 66 | * 67 | * @param groupId The group ID for a channel unit. 68 | * 69 | * @return A channel unit instance or `nil`. 70 | */ 71 | - (id)channelUnitForGroupId:(NSString *)groupId; 72 | 73 | @end 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSChannelProtocol.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSEnable.h" 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | @protocol MSChannelDelegate; 8 | 9 | /** 10 | * `MSChannelProtocol` contains the essential operations of a channel. Channels are broadly responsible for enqueuing logs to be sent to the 11 | * backend and/or stored on disk. 12 | */ 13 | @protocol MSChannelProtocol 14 | 15 | /** 16 | * Add delegate. 17 | * 18 | * @param delegate delegate. 19 | */ 20 | - (void)addDelegate:(id)delegate; 21 | 22 | /** 23 | * Remove delegate. 24 | * 25 | * @param delegate delegate. 26 | */ 27 | - (void)removeDelegate:(id)delegate; 28 | 29 | /** 30 | * Pause operations, logs will be stored but not sent. 31 | * 32 | * @param identifyingObject Object used to identify the pause request. 33 | * 34 | * @discussion A paused channel doesn't forward logs to the ingestion. The identifying object used to pause the channel can be any unique 35 | * object. The same identifying object must be used to call resume. For simplicity if the caller is the one owning the channel then @c self 36 | * can be used as identifying object. 37 | * 38 | * @see resumeWithIdentifyingObject: 39 | */ 40 | - (void)pauseWithIdentifyingObject:(id)identifyingObject; 41 | 42 | /** 43 | * Resume operations, logs can be sent again. 44 | * 45 | * @param identifyingObject Object used to passed to the pause method. 46 | * 47 | * @discussion The channel only resume when all the outstanding identifying objects have been resumed. 48 | * 49 | * @see pauseWithIdentifyingObject: 50 | */ 51 | - (void)resumeWithIdentifyingObject:(id)identifyingObject; 52 | 53 | @end 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSConstants.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | * Log Levels 5 | */ 6 | typedef NS_ENUM(NSUInteger, MSLogLevel) { 7 | 8 | /** 9 | * Logging will be very chatty 10 | */ 11 | MSLogLevelVerbose = 2, 12 | 13 | /** 14 | * Debug information will be logged 15 | */ 16 | MSLogLevelDebug = 3, 17 | 18 | /** 19 | * Information will be logged 20 | */ 21 | MSLogLevelInfo = 4, 22 | 23 | /** 24 | * Errors and warnings will be logged 25 | */ 26 | MSLogLevelWarning = 5, 27 | 28 | /** 29 | * Errors will be logged 30 | */ 31 | MSLogLevelError = 6, 32 | 33 | /** 34 | * Only critical errors will be logged 35 | */ 36 | MSLogLevelAssert = 7, 37 | 38 | /** 39 | * Logging is disabled 40 | */ 41 | MSLogLevelNone = 99 42 | }; 43 | 44 | typedef NSString * (^MSLogMessageProvider)(void); 45 | typedef void (^MSLogHandler)(MSLogMessageProvider messageProvider, MSLogLevel logLevel, NSString *tag, const char *file, 46 | const char *function, uint line); 47 | 48 | /** 49 | * Channel priorities, check the kMSPriorityCount if you add a new value. 50 | * The order matters here! Values NEED to range from low priority to high priority. 51 | */ 52 | typedef NS_ENUM(NSInteger, MSPriority) { MSPriorityBackground, MSPriorityDefault, MSPriorityHigh }; 53 | static short const kMSPriorityCount = MSPriorityHigh + 1; 54 | 55 | /** 56 | * The priority by which the modules are initialized. 57 | * MSPriorityMax is reserved for only 1 module and this needs to be Crashes. 58 | * Crashes needs to be initialized first to catch crashes in our other SDK Modules (which will hopefully never happen) and to avoid losing 59 | * any log at crash time. 60 | */ 61 | typedef NS_ENUM(NSInteger, MSInitializationPriority) { 62 | MSInitializationPriorityDefault = 500, 63 | MSInitializationPriorityHigh = 750, 64 | MSInitializationPriorityMax = 999 65 | }; 66 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSCustomProperties.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | * Custom properties builder. 5 | * Collects multiple properties to send in one log. 6 | */ 7 | @interface MSCustomProperties : NSObject 8 | 9 | /** 10 | * Set the specified property value with the specified key. 11 | * If the properties previously contained a property for the key, the old value is replaced. 12 | * 13 | * @param key Key with which the specified value is to be set. 14 | * @param value Value to be set with the specified key. 15 | * 16 | * @return This instance. 17 | */ 18 | - (instancetype)setString:(NSString *)value forKey:(NSString *)key; 19 | 20 | /** 21 | * Set the specified property value with the specified key. 22 | * If the properties previously contained a property for the key, the old value is replaced. 23 | * 24 | * @param key Key with which the specified value is to be set. 25 | * @param value Value to be set with the specified key. 26 | * 27 | * @return This instance. 28 | */ 29 | - (instancetype)setNumber:(NSNumber *)value forKey:(NSString *)key; 30 | 31 | /** 32 | * Set the specified property value with the specified key. 33 | * If the properties previously contained a property for the key, the old value is replaced. 34 | * 35 | * @param key Key with which the specified value is to be set. 36 | * @param value Value to be set with the specified key. 37 | * 38 | * @return This instance. 39 | */ 40 | - (instancetype)setBool:(BOOL)value forKey:(NSString *)key; 41 | 42 | /** 43 | * Set the specified property value with the specified key. 44 | * If the properties previously contained a property for the key, the old value is replaced. 45 | * 46 | * @param key Key with which the specified value is to be set. 47 | * @param value Value to be set with the specified key. 48 | * 49 | * @return This instance. 50 | */ 51 | - (instancetype)setDate:(NSDate *)value forKey:(NSString *)key; 52 | 53 | /** 54 | * Clear the property for the specified key. 55 | * 56 | * @param key Key whose mapping is to be cleared. 57 | * 58 | * @return This instance. 59 | */ 60 | - (instancetype)clearPropertyForKey:(NSString *)key; 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSDevice.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSWrapperSdk.h" 4 | 5 | @interface MSDevice : MSWrapperSdk 6 | 7 | /* 8 | * Name of the SDK. Consists of the name of the SDK and the platform, e.g. "appcenter.ios", "appcenter.android" 9 | */ 10 | @property(nonatomic, copy, readonly) NSString *sdkName; 11 | 12 | /* 13 | * Version of the SDK in semver format, e.g. "1.2.0" or "0.12.3-alpha.1". 14 | */ 15 | @property(nonatomic, copy, readonly) NSString *sdkVersion; 16 | 17 | /* 18 | * Device model (example: iPad2,3). 19 | */ 20 | @property(nonatomic, copy, readonly) NSString *model; 21 | 22 | /* 23 | * Device manufacturer (example: HTC). 24 | */ 25 | @property(nonatomic, copy, readonly) NSString *oemName; 26 | 27 | /* 28 | * OS name (example: iOS). 29 | */ 30 | @property(nonatomic, copy, readonly) NSString *osName; 31 | 32 | /* 33 | * OS version (example: 9.3.0). 34 | */ 35 | @property(nonatomic, copy, readonly) NSString *osVersion; 36 | 37 | /* 38 | * OS build code (example: LMY47X). [optional] 39 | */ 40 | @property(nonatomic, copy, readonly) NSString *osBuild; 41 | 42 | /* 43 | * API level when applicable like in Android (example: 15). [optional] 44 | */ 45 | @property(nonatomic, copy, readonly) NSNumber *osApiLevel; 46 | 47 | /* 48 | * Language code (example: en_US). 49 | */ 50 | @property(nonatomic, copy, readonly) NSString *locale; 51 | 52 | /* 53 | * The offset in minutes from UTC for the device time zone, including daylight savings time. 54 | */ 55 | @property(nonatomic, readonly) NSNumber *timeZoneOffset; 56 | 57 | /* 58 | * Screen size of the device in pixels (example: 640x480). 59 | */ 60 | @property(nonatomic, copy, readonly) NSString *screenSize; 61 | 62 | /* 63 | * Application version name, e.g. 1.1.0 64 | */ 65 | @property(nonatomic, copy, readonly) NSString *appVersion; 66 | 67 | /* 68 | * Carrier name (for mobile devices). [optional] 69 | */ 70 | @property(nonatomic, copy, readonly) NSString *carrierName; 71 | 72 | /* 73 | * Carrier country code (for mobile devices). [optional] 74 | */ 75 | @property(nonatomic, copy, readonly) NSString *carrierCountry; 76 | 77 | /* 78 | * The app's build number, e.g. 42. 79 | */ 80 | @property(nonatomic, copy, readonly) NSString *appBuild; 81 | 82 | /* 83 | * The bundle identifier, package identifier, or namespace, depending on what the individual plattforms use, .e.g com.microsoft.example. 84 | * [optional] 85 | */ 86 | @property(nonatomic, copy, readonly) NSString *appNamespace; 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSEnable.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | * Protocol to define an instance that can be enabled/disabled. 5 | */ 6 | @protocol MSEnable 7 | 8 | @required 9 | 10 | /** 11 | * Enable/disable this instance and delete data on disabled state. 12 | * 13 | * @param isEnabled A boolean value set to YES to enable the instance or NO to disable it. 14 | * @param deleteData A boolean value set to YES to delete data or NO to keep it. 15 | */ 16 | - (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSLog.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MSDevice; 4 | 5 | @protocol MSLog 6 | 7 | /** 8 | * Log type. 9 | */ 10 | @property(nonatomic, copy) NSString *type; 11 | 12 | /** 13 | * Log timestamp. 14 | */ 15 | @property(nonatomic) NSDate *timestamp; 16 | 17 | /** 18 | * A session identifier is used to correlate logs together. A session is an abstract concept in the API and is not necessarily an analytics 19 | * session, it can be used to only track crashes. 20 | */ 21 | @property(nonatomic, copy) NSString *sid; 22 | 23 | /** 24 | * Optional distribution group ID value. 25 | */ 26 | @property(nonatomic, copy) NSString *distributionGroupId; 27 | 28 | /** 29 | * Optional user identifier. 30 | */ 31 | @property(nonatomic, copy) NSString *userId; 32 | 33 | /** 34 | * Device properties associated to this log. 35 | */ 36 | @property(nonatomic) MSDevice *device; 37 | 38 | /** 39 | * Transient object tag. For example, a log can be tagged with a transmission target. We do this currently to prevent properties being 40 | * applied retroactively to previous logs by comparing their tags. 41 | */ 42 | @property(nonatomic) NSObject *tag; 43 | 44 | /** 45 | * Checks if the object's values are valid. 46 | * 47 | * @return YES, if the object is valid. 48 | */ 49 | - (BOOL)isValid; 50 | 51 | /** 52 | * Adds a transmission target token that this log should be sent to. 53 | * 54 | * @param token The transmission target token. 55 | */ 56 | - (void)addTransmissionTargetToken:(NSString *)token; 57 | 58 | /** 59 | * Gets all transmission target tokens that this log should be sent to. 60 | * 61 | * @returns Collection of transmission target tokens that this log should be sent to. 62 | */ 63 | - (NSSet *)transmissionTargetTokens; 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSLogWithProperties.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSAbstractLog.h" 4 | 5 | @interface MSLogWithProperties : MSAbstractLog 6 | 7 | /** 8 | * Additional key/value pair parameters. [optional] 9 | */ 10 | @property(nonatomic) NSDictionary *properties; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSLogger.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSConstants.h" 4 | 5 | #define MSLog(_level, _tag, _message) \ 6 | [MSLogger logMessage:_message level:_level tag:_tag file:__FILE__ function:__PRETTY_FUNCTION__ line:__LINE__] 7 | #define MSLogAssert(tag, format, ...) \ 8 | MSLog(MSLogLevelAssert, tag, (^{ \ 9 | return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 10 | })) 11 | #define MSLogError(tag, format, ...) \ 12 | MSLog(MSLogLevelError, tag, (^{ \ 13 | return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 14 | })) 15 | #define MSLogWarning(tag, format, ...) \ 16 | MSLog(MSLogLevelWarning, tag, (^{ \ 17 | return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 18 | })) 19 | #define MSLogInfo(tag, format, ...) \ 20 | MSLog(MSLogLevelInfo, tag, (^{ \ 21 | return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 22 | })) 23 | #define MSLogDebug(tag, format, ...) \ 24 | MSLog(MSLogLevelDebug, tag, (^{ \ 25 | return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 26 | })) 27 | #define MSLogVerbose(tag, format, ...) \ 28 | MSLog(MSLogLevelVerbose, tag, (^{ \ 29 | return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 30 | })) 31 | 32 | @interface MSLogger : NSObject 33 | 34 | + (void)logMessage:(MSLogMessageProvider)messageProvider 35 | level:(MSLogLevel)loglevel 36 | tag:(NSString *)tag 37 | file:(const char *)file 38 | function:(const char *)function 39 | line:(uint)line; 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSService.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | * Protocol declaring service logic. 5 | */ 6 | @protocol MSService 7 | 8 | /** 9 | * Enable/disable this service. 10 | * 11 | * @param isEnabled whether this service is enabled or not. 12 | * 13 | * @see isEnabled 14 | */ 15 | + (void)setEnabled:(BOOL)isEnabled; 16 | 17 | /** 18 | * Is this service enabled. 19 | * 20 | * @return a boolean whether this service is enabled or not. 21 | * 22 | * @see setEnabled: 23 | */ 24 | + (BOOL)isEnabled; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSServiceAbstract.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSService.h" 4 | 5 | @protocol MSChannelGroupProtocol; 6 | 7 | /** 8 | * Abstraction of services common logic. 9 | * This class is intended to be subclassed only not instantiated directly. 10 | */ 11 | @interface MSServiceAbstract : NSObject 12 | 13 | /** 14 | * The flag indicates whether the service is started from application or not. 15 | */ 16 | @property(nonatomic) BOOL startedFromApplication; 17 | 18 | /** 19 | * Start this service with a channel group. Also sets the flag that indicates that a service has been started. 20 | * 21 | * @param channelGroup channel group used to persist and send logs. 22 | * @param appSecret app secret for the SDK. 23 | * @param token default transmission target token for this service. 24 | * @param fromApplication indicates whether the service started from an application or not. 25 | */ 26 | - (void)startWithChannelGroup:(id)channelGroup 27 | appSecret:(NSString *)appSecret 28 | transmissionTargetToken:(NSString *)token 29 | fromApplication:(BOOL)fromApplication; 30 | 31 | /** 32 | * Update configuration when the service requires to start again. This method should only be called if the service is started from libraries 33 | * and then is being started from an application. 34 | * 35 | * @param appSecret app secret for the SDK. 36 | * @param token default transmission target token for this service. 37 | */ 38 | - (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token; 39 | 40 | /** 41 | * Checks if the service needs the application secret. 42 | * 43 | * @return `YES` if the application secret is required, `NO` otherwise. 44 | */ 45 | - (BOOL)isAppSecretRequired; 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSWrapperLogger.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSConstants.h" 4 | 5 | /** 6 | * This is a utility for producing App Center style log messages. It is only intended for use by App Center services and wrapper SDKs of App 7 | * Center. 8 | */ 9 | @interface MSWrapperLogger : NSObject 10 | 11 | + (void)MSWrapperLog:(MSLogMessageProvider)message tag:(NSString *)tag level:(MSLogLevel)level; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Headers/MSWrapperSdk.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MSWrapperSdk : NSObject 4 | 5 | /* 6 | * Version of the wrapper SDK. When the SDK is embedding another base SDK (for example Xamarin.Android wraps Android), the Xamarin specific 7 | * version is populated into this field while sdkVersion refers to the original Android SDK. [optional] 8 | */ 9 | @property(nonatomic, copy, readonly) NSString *wrapperSdkVersion; 10 | 11 | /* 12 | * Name of the wrapper SDK (examples: Xamarin, Cordova). [optional] 13 | */ 14 | @property(nonatomic, copy, readonly) NSString *wrapperSdkName; 15 | 16 | /* 17 | * Version of the wrapper technology framework (Xamarin runtime version or ReactNative or Cordova etc...). [optional] 18 | */ 19 | @property(nonatomic, copy, readonly) NSString *wrapperRuntimeVersion; 20 | 21 | /* 22 | * Label that is used to identify application code 'version' released via Live Update beacon running on device. 23 | */ 24 | @property(nonatomic, copy, readonly) NSString *liveUpdateReleaseLabel; 25 | 26 | /* 27 | * Identifier of environment that current application release belongs to, deployment key then maps to environment like Production, Staging. 28 | */ 29 | @property(nonatomic, copy, readonly) NSString *liveUpdateDeploymentKey; 30 | 31 | /* 32 | * Hash of all files (ReactNative or Cordova) deployed to device via LiveUpdate beacon. Helps identify the Release version on device or need 33 | * to download updates in future 34 | */ 35 | @property(nonatomic, copy, readonly) NSString *liveUpdatePackageHash; 36 | 37 | - (instancetype)initWithWrapperSdkVersion:(NSString *)wrapperSdkVersion 38 | wrapperSdkName:(NSString *)wrapperSdkName 39 | wrapperRuntimeVersion:(NSString *)wrapperRuntimeVersion 40 | liveUpdateReleaseLabel:(NSString *)liveUpdateReleaseLabel 41 | liveUpdateDeploymentKey:(NSString *)liveUpdateDeploymentKey 42 | liveUpdatePackageHash:(NSString *)liveUpdatePackageHash; 43 | 44 | /** 45 | * Checks if the object's values are valid. 46 | * 47 | * @return YES, if the object is valid. 48 | */ 49 | - (BOOL)isValid; 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenter.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module AppCenter { 2 | umbrella header "AppCenter.h" 3 | 4 | export * 5 | module * { export * } 6 | 7 | link framework "Foundation" 8 | link framework "CoreTelephony" 9 | link framework "SystemConfiguration" 10 | link framework "UIKit" 11 | link "sqlite3" 12 | link "z" 13 | } 14 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/AppCenterCrashes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Frameworks/AppCenter/AppCenterCrashes.framework/AppCenterCrashes -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/AppCenterCrashes.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSCrashHandlerSetupDelegate.h" 4 | #import "MSCrashes.h" 5 | #import "MSCrashesDelegate.h" 6 | #import "MSErrorAttachmentLog+Utility.h" 7 | #import "MSErrorAttachmentLog.h" 8 | #import "MSWrapperCrashesHelper.h" 9 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSAbstractLog.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MSAbstractLog : NSObject 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSCrashHandlerSetupDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | * This is required for Wrapper SDKs that need to provide custom behavior surrounding the setup of crash handlers. 5 | */ 6 | @protocol MSCrashHandlerSetupDelegate 7 | 8 | @optional 9 | 10 | /** 11 | * Callback method that will be called immediately before crash handlers are set up. 12 | */ 13 | - (void)willSetUpCrashHandlers; 14 | 15 | /** 16 | * Callback method that will be called immediately after crash handlers are set up. 17 | */ 18 | - (void)didSetUpCrashHandlers; 19 | 20 | /** 21 | * Callback method that gets a value indicating whether the SDK should enable an uncaught exception handler. 22 | * 23 | * @return YES if SDK should enable uncaught exception handler, otherwise NO. 24 | * 25 | * @discussion Do not register an UncaughtExceptionHandler for Xamarin as we rely on the Xamarin runtime to report NSExceptions. Registering 26 | * our own UncaughtExceptionHandler will cause the Xamarin debugger to not work properly (it will not stop for NSExceptions). 27 | */ 28 | - (BOOL)shouldEnableUncaughtExceptionHandler; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSCrashesDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MSCrashes; 4 | @class MSErrorReport; 5 | @class MSErrorAttachmentLog; 6 | 7 | @protocol MSCrashesDelegate 8 | 9 | @optional 10 | 11 | /** 12 | * Callback method that will be called before processing errors. 13 | * 14 | * @param crashes The instance of MSCrashes. 15 | * @param errorReport The errorReport that will be sent. 16 | * 17 | * @discussion Crashes will send logs to the server or discard/delete logs based on this method's return value. 18 | */ 19 | - (BOOL)crashes:(MSCrashes *)crashes shouldProcessErrorReport:(MSErrorReport *)errorReport; 20 | 21 | /** 22 | * Callback method that will be called before each error will be send to the server. 23 | * 24 | * @param crashes The instance of MSCrashes. 25 | * @param errorReport The errorReport that will be sent. 26 | * 27 | * @discussion Use this callback to display custom UI while crashes are sent to the server. 28 | */ 29 | - (void)crashes:(MSCrashes *)crashes willSendErrorReport:(MSErrorReport *)errorReport; 30 | 31 | /** 32 | * Callback method that will be called in case the SDK was unable to send an error report to the server. 33 | * 34 | * @param crashes The instance of MSCrashes. 35 | * @param errorReport The errorReport that App Center sent. 36 | * 37 | * @discussion Use this method to hide your custom UI. 38 | */ 39 | - (void)crashes:(MSCrashes *)crashes didSucceedSendingErrorReport:(MSErrorReport *)errorReport; 40 | 41 | /** 42 | * Callback method that will be called in case the SDK was unable to send an error report to the server. 43 | * 44 | * @param crashes The instance of MSCrashes. 45 | * @param errorReport The errorReport that App Center tried to send. 46 | * @param error The error that occurred. 47 | */ 48 | - (void)crashes:(MSCrashes *)crashes didFailSendingErrorReport:(MSErrorReport *)errorReport withError:(NSError *)error; 49 | 50 | /** 51 | * Method to get the attachments associated to an error report. 52 | * 53 | * @param crashes The instance of MSCrashes. 54 | * @param errorReport The errorReport associated with the returned attachments. 55 | * 56 | * @return The attachments associated with the given error report or nil if the error report doesn't have any attachments. 57 | * 58 | * @discussion Implement this method if you want attachments to the given error report. 59 | */ 60 | - (NSArray *)attachmentsWithCrashes:(MSCrashes *)crashes forErrorReport:(MSErrorReport *)errorReport; 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSErrorAttachmentLog+Utility.h: -------------------------------------------------------------------------------- 1 | #import "MSErrorAttachmentLog.h" 2 | 3 | // Exporting symbols for category. 4 | extern NSString *MSMSErrorLogAttachmentLogUtilityCategory; 5 | 6 | @interface MSErrorAttachmentLog (Utility) 7 | 8 | /** 9 | * Create an attachment with a given filename and text. 10 | * 11 | * @param filename The filename the attachment should get. If nil will get an automatically generated filename. 12 | * @param text The attachment text. 13 | * 14 | * @return An instance of `MSErrorAttachmentLog`. 15 | */ 16 | + (MSErrorAttachmentLog *)attachmentWithText:(NSString *)text filename:(NSString *)filename; 17 | 18 | /** 19 | * Create an attachment with a given filename and `NSData` object. 20 | * 21 | * @param filename The filename the attachment should get. If nil will get an automatically generated filename. 22 | * @param data The attachment data as NSData. 23 | * @param contentType The content type of your data as MIME type. 24 | * 25 | * @return An instance of `MSErrorAttachmentLog`. 26 | */ 27 | + (MSErrorAttachmentLog *)attachmentWithBinary:(NSData *)data filename:(NSString *)filename contentType:(NSString *)contentType; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSErrorAttachmentLog.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSAbstractLog.h" 4 | 5 | /** 6 | * Error attachment log. 7 | */ 8 | @interface MSErrorAttachmentLog : MSAbstractLog 9 | 10 | /** 11 | * Content type (text/plain for text). 12 | */ 13 | @property(nonatomic, copy) NSString *contentType; 14 | 15 | /** 16 | * File name. 17 | */ 18 | @property(nonatomic, copy) NSString *filename; 19 | 20 | /** 21 | * The attachment data. 22 | */ 23 | @property(nonatomic, copy) NSData *data; 24 | 25 | /** 26 | * Initialize an attachment with a given filename and `NSData` object. 27 | * 28 | * @param filename The filename the attachment should get. If nil will get an automatically generated filename. 29 | * @param data The attachment data as `NSData`. 30 | * @param contentType The content type of your data as MIME type. 31 | * 32 | * @return An instance of `MSErrorAttachmentLog`. 33 | */ 34 | - (instancetype)initWithFilename:(NSString *)filename attachmentBinary:(NSData *)data contentType:(NSString *)contentType; 35 | 36 | /** 37 | * Initialize an attachment with a given filename and text. 38 | * 39 | * @param filename The filename the attachment should get. If nil will get an automatically generated filename. 40 | * @param text The attachment text. 41 | * 42 | * @return An instance of `MSErrorAttachmentLog`. 43 | */ 44 | - (instancetype)initWithFilename:(NSString *)filename attachmentText:(NSString *)text; 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSErrorReport.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MSDevice; 4 | 5 | @interface MSErrorReport : NSObject 6 | 7 | /** 8 | * UUID for the crash report. 9 | */ 10 | @property(nonatomic, copy, readonly) NSString *incidentIdentifier; 11 | 12 | /** 13 | * UUID for the app installation on the device. 14 | */ 15 | @property(nonatomic, copy, readonly) NSString *reporterKey; 16 | 17 | /** 18 | * Signal that caused the crash. 19 | */ 20 | @property(nonatomic, copy, readonly) NSString *signal; 21 | 22 | /** 23 | * Exception name that triggered the crash, nil if the crash was not caused by an exception. 24 | */ 25 | @property(nonatomic, copy, readonly) NSString *exceptionName; 26 | 27 | /** 28 | * Exception reason, nil if the crash was not caused by an exception. 29 | */ 30 | @property(nonatomic, copy, readonly) NSString *exceptionReason; 31 | 32 | /** 33 | * Date and time the app started, nil if unknown. 34 | */ 35 | @property(nonatomic, readonly) NSDate *appStartTime; 36 | 37 | /** 38 | * Date and time the error occurred, nil if unknown 39 | */ 40 | @property(nonatomic, readonly) NSDate *appErrorTime; 41 | 42 | /** 43 | * Device information of the app when it crashed. 44 | */ 45 | @property(nonatomic, readonly) MSDevice *device; 46 | 47 | /** 48 | * Identifier of the app process that crashed. 49 | */ 50 | @property(nonatomic, readonly, assign) NSUInteger appProcessIdentifier; 51 | 52 | // TODO Please review this doc that contains method name which doesn't exist. 53 | /** 54 | * Indicates if the app was killed while being in foreground from the iOS. 55 | * 56 | * If `[MSCrashes enableAppNotTerminatingCleanlyDetection]` is enabled, use this on startup to check if the app starts the first time after 57 | * it was killed by iOS in the previous session. 58 | * 59 | * This can happen if it consumed too much memory or the watchdog killed the app because it took too long to startup or blocks the main 60 | * thread for too long, or other reasons. See Apple documentation: 61 | * https://developer.apple.com/library/ios/qa/qa1693/_index.html. 62 | * 63 | * See `[MSCrashes enableAppNotTerminatingCleanlyDetection]` for more details about which kind of kills can be detected. 64 | * 65 | * @return YES if the details represent an app kill instead of a crash 66 | * 67 | * @warning This property only has a correct value, once `[BITHockeyManager startManager]` was invoked! In addition, it is automatically 68 | * disabled while a debugger session is active! 69 | * 70 | * @see `[MSCrashes enableAppNotTerminatingCleanlyDetection]` 71 | * @see `[MSCrashes didReceiveMemoryWarningInLastSession]` 72 | */ 73 | - (BOOL)isAppKill; 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSService.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | * Protocol declaring service logic. 5 | */ 6 | @protocol MSService 7 | 8 | /** 9 | * Enable/disable this service. 10 | * 11 | * @param isEnabled whether this service is enabled or not. 12 | * 13 | * @see isEnabled 14 | */ 15 | + (void)setEnabled:(BOOL)isEnabled; 16 | 17 | /** 18 | * Is this service enabled. 19 | * 20 | * @return a boolean whether this service is enabled or not. 21 | * 22 | * @see setEnabled: 23 | */ 24 | + (BOOL)isEnabled; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSServiceAbstract.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSService.h" 4 | 5 | @protocol MSChannelGroupProtocol; 6 | 7 | /** 8 | * Abstraction of services common logic. 9 | * This class is intended to be subclassed only not instantiated directly. 10 | */ 11 | @interface MSServiceAbstract : NSObject 12 | 13 | /** 14 | * The flag indicates whether the service is started from application or not. 15 | */ 16 | @property(nonatomic) BOOL startedFromApplication; 17 | 18 | /** 19 | * Start this service with a channel group. Also sets the flag that indicates that a service has been started. 20 | * 21 | * @param channelGroup channel group used to persist and send logs. 22 | * @param appSecret app secret for the SDK. 23 | * @param token default transmission target token for this service. 24 | * @param fromApplication indicates whether the service started from an application or not. 25 | */ 26 | - (void)startWithChannelGroup:(id)channelGroup 27 | appSecret:(NSString *)appSecret 28 | transmissionTargetToken:(NSString *)token 29 | fromApplication:(BOOL)fromApplication; 30 | 31 | /** 32 | * Update configuration when the service requires to start again. This method should only be called if the service is started from libraries 33 | * and then is being started from an application. 34 | * 35 | * @param appSecret app secret for the SDK. 36 | * @param token default transmission target token for this service. 37 | */ 38 | - (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token; 39 | 40 | /** 41 | * Checks if the service needs the application secret. 42 | * 43 | * @return `YES` if the application secret is required, `NO` otherwise. 44 | */ 45 | - (BOOL)isAppSecretRequired; 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Headers/MSWrapperCrashesHelper.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "MSCrashHandlerSetupDelegate.h" 4 | 5 | @class MSErrorReport; 6 | @class MSErrorAttachmentLog; 7 | 8 | /** 9 | * This general class allows wrappers to supplement the Crashes SDK with their own behavior. 10 | */ 11 | @interface MSWrapperCrashesHelper : NSObject 12 | 13 | /** 14 | * Sets the crash handler setup delegate. 15 | * 16 | * @param delegate The delegate to set. 17 | */ 18 | + (void)setCrashHandlerSetupDelegate:(id)delegate; 19 | 20 | /** 21 | * Gets the crash handler setup delegate. 22 | * 23 | * @return The delegate being used by Crashes. 24 | */ 25 | + (id)getCrashHandlerSetupDelegate; 26 | 27 | /** 28 | * Enables or disables automatic crash processing. 29 | * 30 | * @param automaticProcessing Passing NO causes SDK not to send reports immediately, even if "Always Send" is true. 31 | */ 32 | + (void)setAutomaticProcessing:(BOOL)automaticProcessing; 33 | 34 | /** 35 | * Gets a list of unprocessed crash reports. Will block until the service starts. 36 | * 37 | * @return An array of unprocessed error reports. 38 | */ 39 | + (NSArray *)unprocessedCrashReports; 40 | 41 | /** 42 | * Resumes processing for a given subset of the unprocessed reports. 43 | * 44 | * @param filteredIds An array containing the errorId/incidentIdentifier of each report that should be sent. 45 | * 46 | * @return YES if should "Always Send" is true. 47 | */ 48 | + (BOOL)sendCrashReportsOrAwaitUserConfirmationForFilteredIds:(NSArray *)filteredIds; 49 | 50 | /** 51 | * Sends error attachments for a particular error report. 52 | * 53 | * @param errorAttachments An array of error attachments that should be sent. 54 | * @param incidentIdentifier The identifier of the error report that the attachments will be associated with. 55 | */ 56 | + (void)sendErrorAttachments:(NSArray *)errorAttachments withIncidentIdentifier:(NSString *)incidentIdentifier; 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /Frameworks/AppCenter/AppCenterCrashes.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module AppCenterCrashes { 2 | umbrella header "AppCenterCrashes.h" 3 | 4 | export * 5 | module * { export * } 6 | 7 | link framework "Foundation" 8 | link "c++" 9 | link "z" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 thoughtbot, inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tropos 2 | ====== 3 | 4 | [![Circle CI](https://circleci.com/gh/thoughtbot/Tropos.svg?style=svg)](https://circleci.com/gh/thoughtbot/Tropos) 5 | 6 | Weather and forecasts for humans. 7 | Information you can act on. 8 | 9 | Most weather apps throw a lot of information at you 10 | but that doesn't answer the question of "What does it feel like outside?". 11 | Tropos answers this by relating the current conditions 12 | to conditions at the same time yesterday. 13 | 14 | [Download on the App Store](https://itunes.apple.com/us/app/tropos-weather-forecasts-for/id955209376?mt=8) 15 | 16 | Setup 17 | ----- 18 | 19 | Run `bin/setup` 20 | 21 | This will: 22 | 23 | - Install `fastlane`, `swiftlint` and `xcpretty` 24 | - Install the carthage dependencies 25 | - Create `Secrets.h`. `TRForecastAPIKey` is the only one required for the 26 | application to run. You can get a key from https://developer.forecast.io. You 27 | should include all keys for production builds. 28 | 29 | Testing 30 | ------- 31 | 32 | Run `bin/test` 33 | 34 | This will run the tests from the command line, and pipe the result through 35 | [XCPretty][]. 36 | 37 | Contributing 38 | ------------ 39 | 40 | See the [CONTRIBUTING] document. 41 | Thank you, [contributors]! 42 | 43 | [CONTRIBUTING]: CONTRIBUTING.md 44 | [contributors]: https://github.com/thoughtbot/Tropos/graphs/contributors 45 | 46 | Need Help? 47 | ---------- 48 | 49 | We offer 1-on-1 coaching. 50 | We can help you with ReactiveCocoa, 51 | getting started writing unit tests, 52 | converting from Objective-C to Swift, 53 | and more. [Get in touch]. 54 | 55 | [Get in touch]: http://coaching.thoughtbot.com/ios/?utm_source=github 56 | 57 | License 58 | ------- 59 | 60 | Tropos is Copyright (c) 2019 thoughtbot, inc. It is free software, 61 | and may be redistributed under the terms specified in the [LICENSE] file. 62 | 63 | [LICENSE]: /LICENSE 64 | 65 | About 66 | ----- 67 | 68 | ![thoughtbot](http://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg) 69 | 70 | Tropos is maintained and funded by thoughtbot, inc. 71 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 72 | 73 | We love open source software! 74 | See [our other projects][community] 75 | or [hire us][hire] to help build your product. 76 | 77 | [community]: https://thoughtbot.com/community?utm_source=github 78 | [hire]: https://thoughtbot.com/hire-us?utm_source=github 79 | [XCPretty]: https://github.com/supermarin/xcpretty 80 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Controllers/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/AcknowledgementsParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AcknowledgementsParser { 4 | let propertyList: [String: Any] 5 | 6 | init?(fileURL: URL) { 7 | if let plist = NSDictionary(contentsOf: fileURL) as? [String: Any] { 8 | self.propertyList = plist 9 | } else { 10 | return nil 11 | } 12 | } 13 | 14 | func displayString() -> String { 15 | let acknowledgements = propertyList["PreferenceSpecifiers"] as? [[String: String]] ?? [] 16 | 17 | return acknowledgements.reduce("") { string, acknowledgement in 18 | string + displayStringForAcknowledgement(acknowledgement) 19 | } 20 | } 21 | 22 | private func displayStringForAcknowledgement(_ acknowledgement: [String: String]) -> String { 23 | let appendNewline: (String) -> String = { "\($0)\n" } 24 | let title = acknowledgement["Title"].map(appendNewline) ?? "" 25 | let footer = acknowledgement["FooterText"].map(appendNewline) ?? "" 26 | return title + footer 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppCenter 2 | import AppCenterCrashes 3 | import os.log 4 | import TroposCore 5 | import UIKit 6 | 7 | @UIApplicationMain 8 | final class AppDelegate: UIResponder, UIApplicationDelegate { 9 | let applicationController = ApplicationController() 10 | var window: UIWindow? 11 | 12 | func application( 13 | _ application: UIApplication, 14 | didFinishLaunchingWithOptions _: [UIApplicationLaunchOptionsKey: Any]? = nil 15 | ) -> Bool { 16 | if isCurrentlyTesting { 17 | return true 18 | } 19 | 20 | assertValidSecrets() 21 | setupAnalytics() 22 | setupCrashReporting() 23 | SettingsController().registerSettings() 24 | AppearanceController.configureAppearance() 25 | applicationController.setMinimumBackgroundFetchInterval(for: application) 26 | 27 | window = UIWindow(frame: UIScreen.main.bounds) 28 | window!.rootViewController = applicationController.rootViewController 29 | window!.makeKeyAndVisible() 30 | 31 | return true 32 | } 33 | 34 | func applicationDidBecomeActive(_: UIApplication) { 35 | applicationController.updateWeather().subscribeError(weatherUpdateFailed) 36 | } 37 | 38 | func application( 39 | _: UIApplication, 40 | performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void 41 | ) { 42 | applicationController.updateWeather().subscribeNext({ _ in 43 | completionHandler(.newData) 44 | }, error: { 45 | weatherUpdateFailed(with: $0) 46 | completionHandler(.failed) 47 | }) 48 | } 49 | } 50 | 51 | private func weatherUpdateFailed(with error: Error!) { 52 | if #available(iOS 10.0, *) { 53 | os_log("Failed to update weather: %{public}@", type: .error, error.localizedDescription) 54 | } else { 55 | NSLog("Failed to update weather: %@", error.localizedDescription) 56 | } 57 | } 58 | 59 | private extension AppDelegate { 60 | var isCurrentlyTesting: Bool { 61 | return UserDefaults.standard.bool(forKey: "TRTesting") 62 | } 63 | 64 | func assertValidSecrets() { 65 | assert(!TRAppCenterSecret.isEmpty, "App Center identifier not set") 66 | assert(!TRForecastAPIKey.isEmpty, "Forecast API key not set") 67 | assert(!TRMixpanelToken.isEmpty, "Mixpanel token not set") 68 | } 69 | 70 | func setupAnalytics() { 71 | #if !DEBUG 72 | AnalyticsController.shared.install() 73 | #endif 74 | } 75 | 76 | func setupCrashReporting() { 77 | #if !DEBUG 78 | MSAppCenter.start(TRAppCenterSecret, withServices: [MSCrashes.self]) 79 | #endif 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/AppearanceController.swift: -------------------------------------------------------------------------------- 1 | import TroposCore 2 | import UIKit 3 | 4 | @objc(TRAppearanceController) final class AppearanceController: NSObject { 5 | @objc static func configureAppearance() { 6 | let navigationBar = UINavigationBar.appearance() 7 | navigationBar.titleTextAttributes = [ 8 | .font: UIFont.defaultLightFont(size: 20)!, 9 | .foregroundColor: UIColor.lighterTextColor, 10 | ] 11 | navigationBar.tintColor = .lighterTextColor 12 | 13 | let attributes: [NSAttributedStringKey: Any] = [ 14 | .font: UIFont.defaultLightFont(size: 17)!, 15 | .foregroundColor: UIColor.lighterTextColor, 16 | ] 17 | UIBarButtonItem.appearance().setTitleTextAttributes(attributes, for: .normal) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/TRAnalyticsController.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | #import "TRAnalyticsEvent.h" 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | NS_SWIFT_NAME(AnalyticsController) 7 | @interface TRAnalyticsController : NSObject 8 | 9 | @property (nonatomic, readonly, class) TRAnalyticsController *sharedController; 10 | 11 | - (void)install; 12 | - (void)trackEventNamed:(NSString *)eventName; 13 | - (void)trackEvent:(id)event; 14 | - (void)trackError:(NSError *)error eventName:(NSString *)eventName; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/TRAnalyticsController.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TRAnalyticsController.h" 3 | 4 | #ifndef DEBUG 5 | #import "Secrets.h" 6 | #endif 7 | 8 | #define DISABLE_MIXPANEL_AB_DESIGNER 9 | 10 | @implementation TRAnalyticsController 11 | 12 | #pragma mark - Initialization 13 | 14 | + (instancetype)sharedController 15 | { 16 | static TRAnalyticsController *controller; 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | controller = [TRAnalyticsController new]; 20 | }); 21 | return controller; 22 | } 23 | 24 | #pragma mark - API 25 | 26 | - (void)install 27 | { 28 | #ifndef DEBUG 29 | [Mixpanel sharedInstanceWithToken:TRMixpanelToken]; 30 | #endif 31 | } 32 | 33 | - (void)trackEventNamed:(NSString *)eventName 34 | { 35 | [[Mixpanel sharedInstance] track:eventName]; 36 | } 37 | 38 | - (void)trackEvent:(id)event 39 | { 40 | [[Mixpanel sharedInstance] track:event.eventName properties:event.eventProperties]; 41 | } 42 | 43 | - (void)trackError:(NSError *)error eventName:(NSString *)eventName 44 | { 45 | [[Mixpanel sharedInstance] track:eventName properties:[self propertiesForError:error]]; 46 | } 47 | 48 | #pragma mark - Private 49 | 50 | - (NSDictionary *)propertiesForError:(NSError *)error 51 | { 52 | NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; 53 | if (error.domain) dictionary[@"ErrorDomain"] = error.domain; 54 | if (error.code) dictionary[@"ErrorCode"] = @(error.code); 55 | if (error.userInfo.count > 0) [dictionary addEntriesFromDictionary:error.userInfo]; 56 | return [dictionary copy]; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/TRApplicationController.h: -------------------------------------------------------------------------------- 1 | @import ReactiveObjC; 2 | @import UIKit; 3 | 4 | @class TRLocationController; 5 | 6 | #import "TRApplication.h" 7 | #import "TRWeatherViewController.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | NS_SWIFT_NAME(ApplicationController) 12 | @interface TRApplicationController : NSObject 13 | 14 | - (instancetype)initWithLocationController:(TRLocationController *)locationController; 15 | 16 | @property (nonatomic) TRWeatherViewController *rootViewController; 17 | 18 | - (RACSignal *)updateWeather; 19 | - (void)setMinimumBackgroundFetchIntervalForApplication:(id)application 20 | NS_SWIFT_NAME(setMinimumBackgroundFetchInterval(for:)); 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/TRApplicationController.m: -------------------------------------------------------------------------------- 1 | @import CoreLocation; 2 | @import TroposCore; 3 | 4 | #import "TRApplication.h" 5 | #import "TRApplicationController.h" 6 | #import "TRWeatherController.h" 7 | #import "Tropos-Swift.h" 8 | #import "Secrets.h" 9 | 10 | @interface TRApplicationController () 11 | 12 | @property (nonatomic) TRWeatherController *weatherController; 13 | @property (nonatomic) TRLocationController *locationController; 14 | 15 | @end 16 | 17 | @implementation TRApplicationController 18 | 19 | - (instancetype)initWithLocationController:(TRLocationController *)locationController 20 | { 21 | self = [super init]; 22 | if (!self) { return nil; } 23 | 24 | self.weatherController = [TRWeatherController new]; 25 | self.locationController = locationController; 26 | 27 | UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; 28 | self.rootViewController = [storyboard instantiateInitialViewController]; 29 | self.rootViewController.controller = self.weatherController; 30 | 31 | return self; 32 | } 33 | 34 | - (instancetype)init 35 | { 36 | return [self initWithLocationController:[TRLocationController new]]; 37 | } 38 | 39 | - (RACSignal *)updateWeather 40 | { 41 | return [self.weatherController.updateWeatherCommand execute:self]; 42 | } 43 | 44 | - (void)setMinimumBackgroundFetchIntervalForApplication:(id)application 45 | { 46 | if ([self.locationController authorizationStatusEqualTo:kCLAuthorizationStatusAuthorizedAlways]) { 47 | [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; 48 | } else { 49 | [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever]; 50 | } 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /Sources/Tropos/Controllers/TRWeatherController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface TRWeatherController : NSObject 4 | 5 | @property (nonatomic, readonly) RACSignal *locationName; 6 | @property (nonatomic, readonly) RACSignal *status; 7 | @property (nonatomic, readonly) RACSignal *conditionsImage; 8 | @property (nonatomic, readonly) RACSignal *conditionsDescription; 9 | @property (nonatomic, readonly) RACSignal *windDescription; 10 | @property (nonatomic, readonly) RACSignal *highLowTemperatureDescription; 11 | @property (nonatomic, readonly) RACSignal *dailyForecastViewModels; 12 | @property (nonatomic, readonly) RACSignal *precipitationDescription; 13 | @property (nonatomic, readonly) RACCommand *updateWeatherCommand; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Extensions/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/CATransition.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension CATransition { 4 | @nonobjc static var fade: CATransition { 5 | let transition = CATransition() 6 | transition.duration = 0.3 7 | transition.type = kCATransitionFade 8 | transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 9 | return transition 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/INInteraction.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | 3 | @available(iOS 12.0, *) 4 | extension INInteraction { 5 | @objc(tr_checkWeatherInteraction) 6 | static var checkWeather: INInteraction { 7 | let intent = CheckWeatherIntent() 8 | intent.suggestedInvocationPhrase = checkWeatherSuggestedPhrase 9 | return INInteraction(intent: intent, response: nil) 10 | } 11 | 12 | private static let checkWeatherSuggestedPhrase = NSString 13 | .deferredLocalizedIntentsString(with: "CheckWeatherSuggestedPhrase") as String 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/NSBundle+TRBundleInfo.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSBundle+TRBundleInfo.h 3 | // Tropos 4 | // 5 | // Created by Klaas Pieter Annema on 11-07-16. 6 | // Copyright © 2016 thoughtbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSBundle (TRBundleInfo) 12 | - (NSString *)versionNumber; 13 | @end 14 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/NSBundle+TRBundleInfo.m: -------------------------------------------------------------------------------- 1 | #import "NSBundle+TRBundleInfo.h" 2 | 3 | @implementation NSBundle (TRBundleInfo) 4 | 5 | - (NSString *)versionNumber 6 | { 7 | return [self objectForInfoDictionaryKey:(id)kCFBundleVersionKey]; 8 | } 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/NSError+TRErrors.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSError (TRErrors) 4 | 5 | + (instancetype)locationUnauthorizedError; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/NSError+TRErrors.m: -------------------------------------------------------------------------------- 1 | #import "NSError+TRErrors.h" 2 | #import "TRErrors.h" 3 | 4 | @implementation NSError (TRErrors) 5 | 6 | + (instancetype)locationUnauthorizedError 7 | { 8 | return [NSError errorWithDomain:TRErrorDomain code:TRErrorLocationUnauthorized userInfo:nil]; 9 | } 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/NSNumber+TRRoundedNumber.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSNumber (TRRoundedNumber) 4 | 5 | - (NSNumber *)roundedNumber; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/NSNumber+TRRoundedNumber.m: -------------------------------------------------------------------------------- 1 | #import "NSNumber+TRRoundedNumber.h" 2 | 3 | @implementation NSNumber (TRRoundedNumber) 4 | 5 | - (NSNumber *)roundedNumber 6 | { 7 | return @(roundf([self floatValue])); 8 | } 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/RACSignal+TROperators.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RACSignal (TROperators) 4 | 5 | /// Multicasts the signal to a subject with capicity 1, then lazily 6 | /// connects to the resulting RACMulticastConnection. 7 | - (RACSignal *)replayLastLazily; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/RACSignal+TROperators.m: -------------------------------------------------------------------------------- 1 | #import "RACSignal+TROperators.h" 2 | 3 | @implementation RACSignal (TROperators) 4 | 5 | - (RACSignal *)replayLastLazily 6 | { 7 | RACMulticastConnection *connection = [self multicast:[RACReplaySubject replaySubjectWithCapacity:1]]; 8 | return [[RACSignal defer:^{ 9 | [connection connect]; 10 | return connection.signal; 11 | }] setNameWithFormat:@"[%@] -replayLastLazily", self.name]; 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/TRSettingsController+TRObservation.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface TRSettingsController (TRObservation) 5 | 6 | - (RACSignal *)unitSystemChanged; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/TRSettingsController+TRObservation.m: -------------------------------------------------------------------------------- 1 | #import "TRSettingsController+TRObservation.h" 2 | 3 | @implementation TRSettingsController (TRObservation) 4 | 5 | - (RACSignal *)unitSystemChanged 6 | { 7 | return [[NSUserDefaults standardUserDefaults] rac_valuesForKeyPath:TRSettingsUnitSystemKey observer:self]; 8 | } 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/TRWeatherUpdate+Analytics.h: -------------------------------------------------------------------------------- 1 | @import TroposCore; 2 | #import "TRAnalyticsEvent.h" 3 | 4 | @interface TRWeatherUpdate (TRAnalytics) 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/TRWeatherUpdate+Analytics.m: -------------------------------------------------------------------------------- 1 | @import CoreLocation; 2 | #import "TRWeatherUpdate+Analytics.h" 3 | 4 | @implementation TRWeatherUpdate (TRAnalytics) 5 | 6 | - (NSString *)eventName 7 | { 8 | return @"Weather Update"; 9 | } 10 | 11 | - (NSDictionary *)eventProperties 12 | { 13 | return (@{ 14 | @"Latitude": [self analyticsLatitude], 15 | @"Longitude": [self analyticsLongitude], 16 | @"City": self.city, 17 | @"State": self.state, 18 | @"Temperature": @(self.currentTemperature.fahrenheitValue), 19 | @"Low Temperature": @(self.currentLow.fahrenheitValue), 20 | @"High Temperature": @(self.currentHigh.fahrenheitValue), 21 | @"Wind Speed": @(self.windSpeed), 22 | @"Wind Bearing": @(self.windBearing), 23 | @"Update Date": self.date 24 | }); 25 | } 26 | 27 | #pragma mark - Analytics Formatters 28 | 29 | - (NSNumber *)analyticsLatitude 30 | { 31 | return [self anonymizeLocationDegrees:self.placemark.location.coordinate.latitude]; 32 | } 33 | 34 | - (NSNumber *)analyticsLongitude 35 | { 36 | return [self anonymizeLocationDegrees:self.placemark.location.coordinate.longitude]; 37 | } 38 | 39 | - (NSNumber *)anonymizeLocationDegrees:(double)degrees 40 | { 41 | return @(round(degrees * 100) / 100); 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/UIApplication+TRReactiveBackgroundTask.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface UIApplication (TRReactiveBackgroundTask) 5 | 6 | - (RACSignal *)tr_backgroundTaskWithSignal:(RACSignal *(^)(void))signalBlock; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/UIApplication+TRReactiveBackgroundTask.m: -------------------------------------------------------------------------------- 1 | #import "TRErrors.h" 2 | #import "UIApplication+TRReactiveBackgroundTask.h" 3 | 4 | @implementation UIApplication (TRReactiveBackgroundTask) 5 | 6 | - (RACSignal *)tr_backgroundTaskWithSignal:(RACSignal *(^)(void))signalBlock 7 | { 8 | return [RACSignal createSignal:^RACDisposable *(id subscriber) { 9 | __block RACDisposable *disposable; 10 | 11 | UIBackgroundTaskIdentifier taskIdentifier = [self beginBackgroundTaskWithExpirationHandler:^{ 12 | [disposable dispose]; 13 | }]; 14 | 15 | if (taskIdentifier == UIBackgroundTaskInvalid) { 16 | NSDictionary *userInfo = @{ 17 | NSLocalizedDescriptionKey: @"Running in the background is not possible." 18 | }; 19 | NSError *error = [NSError errorWithDomain:TRErrorDomain 20 | code:0 21 | userInfo: userInfo]; 22 | [subscriber sendError:error]; 23 | } 24 | 25 | RACSignal *signal = signalBlock(); 26 | disposable = [[signal 27 | finally:^{ 28 | [self endBackgroundTask:taskIdentifier]; 29 | }] 30 | subscribe: subscriber]; 31 | 32 | return disposable; 33 | }]; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/UIImage+TRColorBackdrop.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface UIImage (TRColorBackdrop) 4 | 5 | + (instancetype)colorBackdropImage; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/UIImage+TRColorBackdrop.m: -------------------------------------------------------------------------------- 1 | @import TroposCore; 2 | #import "UIImage+TRColorBackdrop.h" 3 | 4 | @implementation UIImage (TRColorBackdrop) 5 | 6 | + (instancetype)colorBackdropImage 7 | { 8 | static UIImage *image; 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | NSArray *colors = @[[UIColor hotColor], [UIColor warmerColor], [UIColor coldColor], [UIColor coolerColor]]; 12 | CGFloat columnWidth = 20.0f; 13 | 14 | CGFloat imageWidth; 15 | CGFloat imageHeight; 16 | imageWidth = imageHeight = columnWidth * colors.count; 17 | 18 | CGFloat yOffset = columnWidth / 2.0f; 19 | CGFloat topY = -yOffset; 20 | CGFloat bottomY = imageHeight + yOffset; 21 | CGFloat xOffset = imageHeight + columnWidth; 22 | 23 | CGSize contextSize = CGSizeMake(imageWidth, imageHeight); 24 | UIGraphicsBeginImageContextWithOptions(contextSize, YES, 0.0f); 25 | 26 | CGContextRef context = UIGraphicsGetCurrentContext(); 27 | CGContextSetLineWidth(context, columnWidth); 28 | CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, imageHeight)); 29 | 30 | NSInteger index = 0; 31 | 32 | while (index * columnWidth - xOffset <= imageWidth) { 33 | UIColor *color = colors[(NSUInteger)index % colors.count]; 34 | CGContextSetStrokeColorWithColor(context, color.CGColor); 35 | 36 | NSInteger x = index * (NSInteger)columnWidth; 37 | CGContextMoveToPoint(context, x - xOffset, bottomY); 38 | CGContextAddLineToPoint(context, x, topY); 39 | CGContextStrokePath(context); 40 | index++; 41 | } 42 | 43 | image = UIGraphicsGetImageFromCurrentImageContext(); 44 | UIGraphicsEndImageContext(); 45 | image = [image resizableImageWithCapInsets:UIEdgeInsetsZero resizingMode:UIImageResizingModeTile]; 46 | }); 47 | 48 | return image; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/UIScrollView+TRReactiveCocoa.h: -------------------------------------------------------------------------------- 1 | #import 2 | @import UIKit; 3 | 4 | @interface UIScrollView (TRReactiveCocoa) 5 | 6 | @property (nonatomic, readonly) RACSignal *verticalAmountScrolledSignal; 7 | @property (nonatomic, readonly) RACSignal *deceleratingSignal; 8 | 9 | @property (nonatomic, readonly) BOOL isScrolledToBottom; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Sources/Tropos/Extensions/UIScrollView+TRReactiveCocoa.m: -------------------------------------------------------------------------------- 1 | #import "UIScrollView+TRReactiveCocoa.h" 2 | 3 | @implementation UIScrollView (TRReactiveCocoa) 4 | 5 | - (RACSignal *)deceleratingSignal 6 | { 7 | return [[RACObserve(self, contentOffset) map:^id(id value) { 8 | return @(self.decelerating); 9 | }] 10 | distinctUntilChanged] 11 | ; 12 | } 13 | 14 | - (RACSignal *)verticalAmountScrolledSignal 15 | { 16 | return [[RACObserve(self, contentOffset) map:^id(NSValue *contentOffset) { 17 | CGFloat yOffset = (CGFloat)floor(contentOffset.CGPointValue.y); 18 | return @(-1 * yOffset); 19 | }] 20 | distinctUntilChanged] 21 | ; 22 | } 23 | 24 | - (BOOL)isScrolledToBottom 25 | { 26 | return (self.contentOffset.y == self.contentSize.height - CGRectGetHeight(self.bounds)); 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Sources/Tropos/Headers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Headers/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Headers/Tropos-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "CheckWeatherIntent.h" 2 | #import "Secrets.h" 3 | #import "TRAnalyticsController.h" 4 | #import "TRApplicationController.h" 5 | -------------------------------------------------------------------------------- /Sources/Tropos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleSignature 20 | TBOT 21 | CFBundleVersion 22 | 214 23 | ITSAppUsesNonExemptEncryption 24 | 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | troposweather.com 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | NSIncludesSubdomains 36 | 37 | 38 | 39 | 40 | NSPhotoLibraryUsageDescription 41 | Tropos does not access your photo library. 42 | NSUserActivityTypes 43 | 44 | CheckWeatherIntent 45 | 46 | UIAppFonts 47 | 48 | DINNextLTPro-Light.otf 49 | DINNextLTPro-Regular.otf 50 | 51 | UIBackgroundModes 52 | 53 | fetch 54 | remote-notification 55 | 56 | UILaunchStoryboardName 57 | LaunchScreen 58 | UIRequiredDeviceCapabilities 59 | 60 | armv7 61 | 62 | UIStatusBarHidden 63 | 64 | UIStatusBarStyle 65 | UIStatusBarStyleLightContent 66 | UISupportedInterfaceOrientations 67 | 68 | UIInterfaceOrientationPortrait 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Sources/Tropos/Models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Models/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Models/Color.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct Color { 4 | static let cellSelection = UIColor(red: 0.325, green: 0.325, blue: 0.352, alpha: 1) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Tropos/Protocols/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Protocols/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Protocols/TRAnalyticsEvent.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @protocol TRAnalyticsEvent 4 | 5 | @property (nonatomic, readonly) NSString *eventName; 6 | @property (nonatomic, readonly) NSDictionary *eventProperties; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Sources/Tropos/Protocols/TRApplication.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @protocol TRApplication 6 | - (void)setMinimumBackgroundFetchInterval:(NSTimeInterval)minimumBackgroundFetchInterval; 7 | @end 8 | 9 | @interface UIApplication (TRApplication) 10 | @end 11 | 12 | NS_ASSUME_NONNULL_END 13 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Fonts/DINNextLTPro-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Fonts/DINNextLTPro-Light.otf -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Fonts/DINNextLTPro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Fonts/DINNextLTPro-Regular.otf -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/1024 App Icon for the App Store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/1024 App Icon for the App Store.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-167.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-20.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-72.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-42.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-43.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/AppIcon.appiconset/Icon@2x.png -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "settings.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/settings.imageset/settings.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/settings.imageset/settings.pdf -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/temp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "temp.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/temp.imageset/temp.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/temp.imageset/temp.pdf -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/thoughtbot-logo-full.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "thoughtbot-logo-full.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode", 11 | "template-rendering-intent" : "template" 12 | } 13 | } -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/thoughtbot-logo-full.imageset/thoughtbot-logo-full.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/thoughtbot-logo-full.imageset/thoughtbot-logo-full.pdf -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/wind-icon-small.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "wind-icon-small.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Images.xcassets/wind-icon-small.imageset/wind-icon-small.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Images.xcassets/wind-icon-small.imageset/wind-icon-small.pdf -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Nibs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Nibs/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Nibs/CWForecastDetailView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Other-Sources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Other-Sources/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Resources/Storyboards/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Resources/Storyboards/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Resources/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Tropos"; 2 | "NSLocationWhenInUseUsageDescription" = "Tropos will only use your location to discover current weather conditions and forecasts for your area."; 3 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "Tropos will only use your location to stay updated on current weather conditions and forecasts for your area."; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CheckWeatherSuggestedPhrase" = "Check the weather"; 2 | "CheckingWeather" = "Checking Weather..."; 3 | "UpdateFailed" = "Update Failed"; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/it.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Tropos"; 2 | "NSLocationWhenInUseUsageDescription" = "Tropos userà la tua posizione solo per scoprire le condizioni meteo attuali e quelle previste nella la tua zona."; 3 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CheckWeatherSuggestedPhrase" = "Controlla previsioni Meteo"; 2 | "CheckingWeather" = "Controllo il Meteo..."; 3 | "UpdateFailed" = "Update non riuscito"; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/pl-PL.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Tropos"; 2 | "NSLocationWhenInUseUsageDescription" = "Tropos użyje twojej lokalizacji żeby sprawdzić pogodę w twojej okolicy."; 3 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "Tropos będzie używać twojej lokalizacji żeby sprawdzać pogodę w twojej okolicy."; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/pl-PL.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CheckWeatherSuggestedPhrase" = "Sprawdź pogodę"; 2 | "CheckingWeather" = "Sprawdzam pogodę..."; 3 | "UpdateFailed" = "Brak połączenia z serwerem"; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/sv.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Tropos"; 2 | "NSLocationWhenInUseUsageDescription" = "Tropos använder endast din position för att hämta väderförhållanden och prognoser för ditt område."; 3 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "Tropos använder endast din postion för att hålla väderförhållanden och prognoser för ditt område uppdaterade."; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Resources/sv.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CheckWeatherSuggestedPhrase" = "Kontrollera vädret"; 2 | "CheckingWeather" = "Uppdaterar prognos..."; 3 | "UpdateFailed" = "Uppdatering misslyckades"; 4 | -------------------------------------------------------------------------------- /Sources/Tropos/Tropos.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.siri 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.thoughtbot.carlweathers 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Sources/Tropos/ViewControllers/TRWeatherViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @class TRWeatherController; 4 | 5 | @interface TRWeatherViewController : UIViewController 6 | 7 | @property (nonatomic) TRWeatherController *controller; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Sources/Tropos/ViewControllers/TextViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TextViewController: UIViewController { 4 | @IBOutlet var textView: UITextView! 5 | 6 | @objc var text: String? 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | textView.contentInset = UIEdgeInsets.zero 11 | textView.textContainerInset = UIEdgeInsets( 12 | top: 20.0, 13 | left: 20.0, 14 | bottom: 20.0, 15 | right: 20.0 16 | ) 17 | textView.font = UIFont.defaultRegularFont(size: 14) 18 | textView.textColor = .lightText 19 | } 20 | 21 | override func viewWillAppear(_ animated: Bool) { 22 | super.viewWillAppear(animated) 23 | textView.text = text 24 | } 25 | 26 | override func viewDidLayoutSubviews() { 27 | super.viewDidLayoutSubviews() 28 | textView.contentOffset = .zero 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Tropos/ViewControllers/WebViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class WebViewController: UIViewController, UIWebViewDelegate { 4 | @IBOutlet var webView: UIWebView! 5 | 6 | @objc var url = URL(string: "about:blank")! 7 | 8 | override func viewWillAppear(_ animated: Bool) { 9 | super.viewWillAppear(animated) 10 | webView.loadRequest(URLRequest(url: url)) 11 | webView.scrollView.contentInset = UIEdgeInsets( 12 | top: 20.0, 13 | left: 20.0, 14 | bottom: 20.0, 15 | right: 20.0 16 | ) 17 | } 18 | 19 | @IBAction func share(_ sender: UIBarButtonItem) { 20 | let activityViewController = UIActivityViewController( 21 | activityItems: [url], 22 | applicationActivities: nil 23 | ) 24 | present(activityViewController, animated: true, completion: nil) 25 | } 26 | 27 | func webViewDidStartLoad(_ webView: UIWebView) { 28 | navigationItem.rightBarButtonItem = .activityIndicatorBarButtonItem() 29 | } 30 | 31 | func webViewDidFinishLoad(_ webView: UIWebView) { 32 | navigationItem.rightBarButtonItem = nil 33 | } 34 | } 35 | 36 | extension UIBarButtonItem { 37 | @objc static func activityIndicatorBarButtonItem() -> UIBarButtonItem { 38 | let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .white) 39 | activityIndicatorView.startAnimating() 40 | return UIBarButtonItem(customView: activityIndicatorView) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/Tropos/Views/.gitkeep -------------------------------------------------------------------------------- /Sources/Tropos/Views/FadingImageView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | private let imageKey = #selector(setter: UIImageView.image).description 4 | 5 | class FadingImageView: UIImageView { 6 | override var image: UIImage? { 7 | didSet { 8 | layer.setValue(image, forKey: imageKey) 9 | } 10 | } 11 | 12 | override func action(for layer: CALayer, forKey event: String) -> CAAction? { 13 | switch event { 14 | case imageKey: 15 | return CATransition.fade 16 | default: 17 | return super.action(for: layer, forKey: event) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/FadingLabel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | private let textKey = #selector(setter: UILabel.text).description 4 | private let attributedTextKey = #selector(setter: UILabel.attributedText).description 5 | 6 | class FadingLabel: UILabel { 7 | override var text: String? { 8 | didSet { 9 | layer.setValue(text, forKey: textKey) 10 | } 11 | } 12 | 13 | override var attributedText: NSAttributedString? { 14 | didSet { 15 | layer.setValue(attributedText, forKey: attributedTextKey) 16 | } 17 | } 18 | 19 | override func action(for layer: CALayer, forKey event: String) -> CAAction? { 20 | switch event { 21 | case textKey, attributedTextKey: 22 | return CATransition.fade 23 | default: 24 | return super.action(for: layer, forKey: event) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRCircularProgressLayer.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface TRCircularProgressLayer : CALayer 4 | 5 | @property (nonatomic) CGFloat progress; 6 | @property (nonatomic) CGFloat radius; 7 | @property (nonatomic) CGFloat outerRingWidth; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRCircularProgressLayer.m: -------------------------------------------------------------------------------- 1 | #import "TRCircularProgressLayer.h" 2 | 3 | static CGPoint TRCGPointMakeIntegral(CGFloat x, CGFloat y) { 4 | return CGPointMake((CGFloat)round(x), (CGFloat)round(y)); 5 | 6 | } 7 | 8 | @implementation TRCircularProgressLayer 9 | 10 | @dynamic progress, radius; 11 | 12 | #pragma mark - Initialization 13 | 14 | - (instancetype)init 15 | { 16 | self = [super init]; 17 | if (!self) return nil; 18 | 19 | self.actions = @{@"bounds": [NSNull null], @"contents": [NSNull null], @"position": [NSNull null]}; 20 | self.contentsScale = [UIScreen mainScreen].scale; 21 | self.needsDisplayOnBoundsChange = YES; 22 | self.outerRingWidth = 3.0f; 23 | self.radius = 15.0f; 24 | 25 | return self; 26 | } 27 | 28 | - (instancetype)initWithLayer:(TRCircularProgressLayer *)layer 29 | { 30 | self = [super initWithLayer:layer]; 31 | if (!self) return nil; 32 | if (![layer isKindOfClass:[TRCircularProgressLayer class]]) return self; 33 | 34 | self.progress = layer.progress; 35 | self.radius = layer.radius; 36 | 37 | return self; 38 | } 39 | 40 | #pragma mark - CALayer 41 | 42 | - (void)drawInContext:(CGContextRef)context 43 | { 44 | CGPoint center = TRCGPointMakeIntegral(CGRectGetWidth(self.bounds) / 2.0f, CGRectGetHeight(self.bounds) / 2.0f); 45 | 46 | CGFloat progress = MIN(self.progress, 1.0f - FLT_EPSILON); 47 | CGFloat radians = (progress * (CGFloat)M_PI * 2.0f) - (CGFloat)M_PI_2; 48 | 49 | CGContextSetFillColorWithColor(context, self.backgroundColor); 50 | CGContextFillRect(context, self.bounds); 51 | 52 | CGContextSetBlendMode(context, kCGBlendModeClear); 53 | 54 | CGContextSetLineWidth(context, self.outerRingWidth); 55 | CGContextSetStrokeColorWithColor(context, [[UIColor clearColor] CGColor]); 56 | CGContextAddArc(context, center.x, center.y, self.radius, 0.0f, (CGFloat)M_PI * 2, YES); 57 | CGContextStrokePath(context); 58 | 59 | if (progress > 0.0f) { 60 | CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]); 61 | CGMutablePathRef progressPath = CGPathCreateMutable(); 62 | CGPathMoveToPoint(progressPath, NULL, center.x, center.y); 63 | CGPathAddArc(progressPath, NULL, center.x, center.y, self.radius, 3.0f * (CGFloat)M_PI_2, radians, false); 64 | CGPathCloseSubpath(progressPath); 65 | CGContextAddPath(context, progressPath); 66 | CGContextFillPath(context); 67 | CGPathRelease(progressPath); 68 | } 69 | } 70 | 71 | + (BOOL)needsDisplayForKey:(NSString *)key 72 | { 73 | if ([[self animatableKeys] containsObject:key]) { 74 | return YES; 75 | } 76 | 77 | return [super needsDisplayForKey:key]; 78 | } 79 | 80 | #pragma mark - Private 81 | 82 | + (NSSet *)animatableKeys 83 | { 84 | static NSSet *keys; 85 | 86 | if (!keys) { 87 | NSString *progressKey = NSStringFromSelector(@selector(progress)); 88 | NSString *maskRadiusKey = NSStringFromSelector(@selector(radius)); 89 | NSString *outerRingWidthKey = NSStringFromSelector(@selector(outerRingWidth)); 90 | keys = [[NSSet alloc] initWithObjects:progressKey, maskRadiusKey, outerRingWidthKey, nil]; 91 | } 92 | 93 | return keys; 94 | } 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRColorBackdropLayer.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface TRColorBackdropLayer : CALayer 4 | 5 | - (void)startAnimating; 6 | - (void)stopAnimating; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRColorBackdropLayer.m: -------------------------------------------------------------------------------- 1 | #import "TRColorBackdropLayer.h" 2 | #import "UIImage+TRColorBackdrop.h" 3 | 4 | static NSString *const TRColorBackdropLayerAnimationKey = @"TRColorBackdropLayerAnimationKey"; 5 | 6 | @implementation TRColorBackdropLayer 7 | 8 | #pragma mark - Initialization 9 | 10 | - (instancetype)init 11 | { 12 | self = [super init]; 13 | if (!self) return nil; 14 | 15 | self.actions = @{@"bounds": [NSNull null], @"position": [NSNull null]}; 16 | self.backgroundColor = [[UIColor colorWithPatternImage:[UIImage colorBackdropImage]] CGColor]; 17 | 18 | return self; 19 | } 20 | 21 | #pragma mark - API 22 | 23 | - (void)startAnimating 24 | { 25 | [self addAnimation:[self positionAnimation] forKey:TRColorBackdropLayerAnimationKey]; 26 | } 27 | 28 | - (void)stopAnimating 29 | { 30 | [self removeAnimationForKey:TRColorBackdropLayerAnimationKey]; 31 | } 32 | 33 | #pragma mark - Private 34 | 35 | - (CABasicAnimation *)positionAnimation 36 | { 37 | CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.x"]; 38 | animation.toValue = @(self.position.x - 80.0f); 39 | animation.repeatCount = HUGE_VALF; 40 | animation.duration = 0.5; 41 | return animation; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRDailyForecastView.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @class TRDailyForecastViewModel; 4 | 5 | @interface TRDailyForecastView : UIView 6 | 7 | @property (nonatomic) TRDailyForecastViewModel *viewModel; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRDailyForecastView.m: -------------------------------------------------------------------------------- 1 | @import TroposCore; 2 | #import "TRDailyForecastView.h" 3 | 4 | @interface TRDailyForecastView () 5 | 6 | @property (weak, nonatomic) IBOutlet UILabel *dayOfWeekLabel; 7 | @property (weak, nonatomic) IBOutlet UIImageView *conditionsImageView; 8 | @property (weak, nonatomic) IBOutlet UILabel *highTemperatureLabel; 9 | @property (weak, nonatomic) IBOutlet UILabel *lowTemperatureLabel; 10 | @property (strong, nonatomic) IBOutlet TRDailyForecastView *contentView; 11 | 12 | @end 13 | 14 | @implementation TRDailyForecastView 15 | 16 | - (void)awakeFromNib 17 | { 18 | [super awakeFromNib]; 19 | [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil]; 20 | [self addSubview:self.contentView]; 21 | } 22 | 23 | - (void)layoutSubviews 24 | { 25 | [super layoutSubviews]; 26 | self.contentView.frame = self.bounds; 27 | } 28 | 29 | - (void)setViewModel:(TRDailyForecastViewModel *)viewModel 30 | { 31 | _viewModel = viewModel; 32 | self.dayOfWeekLabel.text = viewModel.dayOfWeek; 33 | self.conditionsImageView.image = viewModel.conditionsImage; 34 | self.highTemperatureLabel.text = viewModel.highTemperature; 35 | self.lowTemperatureLabel.text = viewModel.lowTemperature; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRNavigationBar.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface TRNavigationBar : UINavigationBar 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRNavigationBar.m: -------------------------------------------------------------------------------- 1 | #import "TRNavigationBar.h" 2 | 3 | @interface TRNavigationBar () 4 | @end 5 | 6 | @implementation TRNavigationBar 7 | 8 | #pragma mark - NSObject 9 | 10 | - (void)awakeFromNib 11 | { 12 | [super awakeFromNib]; 13 | self.shadowImage = [UIImage new]; 14 | [self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRRefreshControl.h: -------------------------------------------------------------------------------- 1 | #import 2 | @import UIKit; 3 | 4 | @class TRRefreshView; 5 | 6 | @interface TRRefreshControl : UIControl 7 | 8 | @property (nonatomic, weak) IBOutlet UIScrollView *scrollView; 9 | 10 | @property (nonatomic) IBInspectable CGFloat refreshTriggerOffset; 11 | @property (nonatomic) IBInspectable CGFloat refreshProgressOffset; 12 | 13 | @property (nonatomic) RACCommand *refreshCommand; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRRefreshLayer.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface TRRefreshLayer : CALayer 4 | 5 | + (instancetype)layerWithMask:(CALayer *)mask; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRRefreshLayer.m: -------------------------------------------------------------------------------- 1 | @import TroposCore; 2 | #import "TRRefreshLayer.h" 3 | 4 | @implementation TRRefreshLayer 5 | 6 | + (instancetype)layerWithMask:(CALayer *)mask 7 | { 8 | TRRefreshLayer *layer = [self layer]; 9 | layer.mask = mask; 10 | return layer; 11 | } 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | 17 | self.actions = @{@"bounds": [NSNull null], @"position": [NSNull null]}; 18 | self.backgroundColor = [[UIColor secondaryBackgroundColor] CGColor]; 19 | 20 | return self; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRRefreshView.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface TRRefreshView : UIView 4 | 5 | @property (nonatomic) CGFloat progress; 6 | @property (nonatomic) CGFloat maskExpansionProgress; 7 | @property (nonatomic) BOOL animating; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRRefreshView.m: -------------------------------------------------------------------------------- 1 | #import "TRRefreshView.h" 2 | #import "TRCircularProgressLayer.h" 3 | #import "TRColorBackdropLayer.h" 4 | #import "TRRefreshLayer.h" 5 | 6 | @interface TRRefreshView () 7 | 8 | @property (nonatomic) TRColorBackdropLayer *backdropLayer; 9 | @property (nonatomic) TRRefreshLayer *refreshLayer; 10 | @property (nonatomic) TRCircularProgressLayer *progressLayer; 11 | 12 | @end 13 | 14 | @implementation TRRefreshView 15 | 16 | - (id)initWithCoder:(NSCoder *)aDecoder 17 | { 18 | self = [super initWithCoder:aDecoder]; 19 | if (!self) return nil; 20 | 21 | [self.layer addSublayer:self.backdropLayer]; 22 | [self.layer addSublayer:self.refreshLayer]; 23 | 24 | return self; 25 | } 26 | 27 | #pragma mark - UIView 28 | 29 | - (void)layoutSubviews 30 | { 31 | [super layoutSubviews]; 32 | self.backdropLayer.frame = ({ 33 | CGRect bounds = self.bounds; 34 | bounds.size.width *= 2.0f; 35 | bounds; 36 | }); 37 | self.refreshLayer.frame = self.bounds; 38 | self.progressLayer.frame = self.bounds; 39 | } 40 | 41 | #pragma mark - API 42 | 43 | - (void)setProgress:(CGFloat)progress 44 | { 45 | self.progressLayer.progress = progress; 46 | } 47 | 48 | - (void)setMaskExpansionProgress:(CGFloat)maskExpansionProgress 49 | { 50 | maskExpansionProgress = MAX(0.0f, MIN(maskExpansionProgress, 1.0f)); 51 | CGFloat radiusDelta = (CGRectGetWidth(self.bounds) / 2.0f + self.progressLayer.outerRingWidth) - 15.0f; 52 | self.progressLayer.radius = (CGFloat)ceil(radiusDelta * maskExpansionProgress) + 15.0f; 53 | } 54 | 55 | - (void)setAnimating:(BOOL)animating 56 | { 57 | if (animating == _animating) return; 58 | _animating = animating; 59 | (_animating)? [self startAnimating] : [self stopAnimating]; 60 | } 61 | 62 | #pragma mark - Private 63 | 64 | - (TRColorBackdropLayer *)backdropLayer 65 | { 66 | if (!_backdropLayer) _backdropLayer = [TRColorBackdropLayer layer]; 67 | return _backdropLayer; 68 | } 69 | 70 | - (TRCircularProgressLayer *)progressLayer 71 | { 72 | if (!_progressLayer) _progressLayer = [TRCircularProgressLayer layer]; 73 | return _progressLayer; 74 | } 75 | 76 | - (TRRefreshLayer *)refreshLayer 77 | { 78 | if (!_refreshLayer) _refreshLayer = [TRRefreshLayer layerWithMask:self.progressLayer]; 79 | return _refreshLayer; 80 | } 81 | 82 | - (void)startAnimating 83 | { 84 | [self.backdropLayer startAnimating]; 85 | } 86 | 87 | - (void)stopAnimating 88 | { 89 | [self.backdropLayer stopAnimating]; 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /Sources/Tropos/Views/TRTableViewCell.swift: -------------------------------------------------------------------------------- 1 | import TroposCore 2 | import UIKit 3 | 4 | class TRTableViewCell: UITableViewCell { 5 | override func awakeFromNib() { 6 | super.awakeFromNib() 7 | textLabel?.font = UIFont.defaultLightFont(size: 18.0) 8 | 9 | let highlightView = UIView() 10 | highlightView.backgroundColor = Color.cellSelection 11 | selectedBackgroundView = highlightView 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/TroposCore/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/.gitkeep -------------------------------------------------------------------------------- /Sources/TroposCore/Controllers/ForecastController.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import ReactiveSwift 3 | import Result 4 | 5 | private var locationNotFound: Error { 6 | return NSError(domain: TRErrorDomain, code: TRError.conditionsResponseLocationNotFound.rawValue) 7 | } 8 | 9 | private var responseFailed: Error { 10 | return NSError(domain: TRErrorDomain, code: TRError.conditionsResponseFailed.rawValue) 11 | } 12 | 13 | private var unexpectedFormat: Error { 14 | return NSError(domain: TRErrorDomain, code: TRError.conditionsResponseUnexpectedFormat.rawValue) 15 | } 16 | 17 | @objc(TRForecastController) 18 | public final class ForecastController: NSObject { 19 | private let baseURL: URL 20 | private let urlSession: URLSession 21 | 22 | @objc(initWithAPIKey:) 23 | public init(apiKey: String) { 24 | var components = URLComponents() 25 | components.scheme = "https" 26 | components.host = "api.forecast.io" 27 | components.path = "/forecast/\(apiKey)" 28 | baseURL = components.url! 29 | 30 | let configuration = URLSessionConfiguration.default 31 | configuration.httpAdditionalHeaders = ["Accept": "application/json"] 32 | configuration.requestCachePolicy = .reloadIgnoringCacheData 33 | urlSession = URLSession(configuration: configuration) 34 | } 35 | 36 | public func fetchWeatherUpdate(for placemark: CLPlacemark) -> SignalProducer { 37 | guard let location = placemark.location else { return SignalProducer(error: AnyError(locationNotFound)) } 38 | 39 | let today = conditionsRequest(for: location.coordinate, date: nil) 40 | let yesterday = conditionsRequest(for: location.coordinate, date: .yesterday) 41 | 42 | return fetch(today).zip(with: fetch(yesterday)).map { 43 | WeatherUpdate(placemark: placemark, currentConditionsJSON: $0, yesterdaysConditionsJSON: $1) 44 | } 45 | } 46 | 47 | private func fetch(_ conditionsRequest: URLRequest) -> SignalProducer<[String: Any], AnyError> { 48 | return urlSession.reactive.data(with: conditionsRequest).attemptMap { 49 | let (data, response) = $0 50 | guard 200 ..< 300 ~= (response as! HTTPURLResponse).statusCode else { throw responseFailed } 51 | 52 | let json = try JSONSerialization.jsonObject(with: data) 53 | 54 | if let object = json as? [String: Any] { 55 | return object 56 | } else { 57 | throw unexpectedFormat 58 | } 59 | } 60 | } 61 | 62 | private func conditionsRequest(for location: CLLocationCoordinate2D, date: Date?) -> URLRequest { 63 | var location = String(format: "%f,%f", location.latitude, location.longitude) 64 | 65 | if let date = date { 66 | location += String(format: ",%.0f", date.timeIntervalSince1970) 67 | } 68 | 69 | var components = URLComponents( 70 | url: baseURL.appendingPathComponent(location), 71 | resolvingAgainstBaseURL: true 72 | )! 73 | 74 | components.queryItems = [ 75 | URLQueryItem(name: "exclude", value: "minutely,hourly,alerts,flags"), 76 | ] 77 | 78 | return URLRequest(url: components.url!) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/TroposCore/Controllers/GeocodeController.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import ReactiveObjCBridge 3 | import ReactiveSwift 4 | import Result 5 | 6 | @objc(TRGeocodeController) 7 | public final class GeocodeController: NSObject { 8 | private let geocoder: Geocoder 9 | 10 | public init(geocoder: Geocoder) { 11 | self.geocoder = geocoder 12 | } 13 | 14 | public convenience override init() { 15 | self.init(geocoder: CLGeocoder()) 16 | } 17 | 18 | public func reverseGeocode(_ location: CLLocation) -> SignalProducer { 19 | return SignalProducer { [geocoder] observer, lifetime in 20 | lifetime.observeEnded(geocoder.cancelGeocode) 21 | 22 | geocoder.reverseGeocodeLocation(location) { placemarks, error in 23 | switch (placemarks, error) { 24 | case let (placemarks?, nil): 25 | observer.send(value: placemarks.first!) 26 | observer.sendCompleted() 27 | case (nil, CLError.geocodeCanceled?): 28 | observer.sendInterrupted() 29 | case let (nil, error?): 30 | observer.send(error: error as! CLError) 31 | default: 32 | fatalError() 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | extension GeocodeController { 40 | @objc public func reverseGeocodeLocation(_ location: CLLocation) -> RACSignal { 41 | return reverseGeocode(location).bridged 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/TroposCore/Controllers/SettingsController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(TRSettingsController) public final class SettingsController: NSObject { 4 | private let locale: Locale 5 | private let userDefaults: UserDefaults 6 | 7 | public var unitSystem: UnitSystem { 8 | get { 9 | let rawUnitSystem = userDefaults.integer(forKey: TRSettingsUnitSystemKey) 10 | return UnitSystem(rawValue: rawUnitSystem)! 11 | } 12 | set { 13 | userDefaults.set(newValue.rawValue, forKey: TRSettingsUnitSystemKey) 14 | } 15 | } 16 | 17 | public var unitSystemChanged: ((UnitSystem) -> Void)? 18 | 19 | @objc public init(locale: Locale, userDefaults: UserDefaults) { 20 | self.locale = locale 21 | self.userDefaults = userDefaults 22 | 23 | super.init() 24 | 25 | NotificationCenter.default.addObserver( 26 | self, 27 | selector: #selector(userDefaultsDidChange(_:)), 28 | name: UserDefaults.didChangeNotification, 29 | object: userDefaults 30 | ) 31 | } 32 | 33 | @objc func userDefaultsDidChange(_ notification: Notification) { 34 | unitSystemChanged?(unitSystem) 35 | } 36 | 37 | @objc public convenience init(locale: Locale) { 38 | self.init(locale: locale, userDefaults: .standard) 39 | } 40 | 41 | public convenience override init() { 42 | self.init(locale: .autoupdatingCurrent) 43 | } 44 | 45 | @objc public func registerSettings() { 46 | registerUnitSystem() 47 | registerLastVersion() 48 | } 49 | 50 | private func registerUnitSystem() { 51 | let unitSystem: UnitSystem = locale.usesMetricSystem ? .metric : .imperial 52 | userDefaults.register(defaults: [TRSettingsUnitSystemKey: unitSystem.rawValue]) 53 | } 54 | 55 | private func registerLastVersion() { 56 | if let version = Bundle.main.versionNumber { 57 | userDefaults.register(defaults: [TRSettingsLastVersionKey: version]) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/CLLocation+TRRecentLocation.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | extension CLLocation { 4 | @objc(tr_isStale) var isStale: Bool { 5 | return Date().timeIntervalSince(timestamp) > 5 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/Date+Relative.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | static var yesterday: Date { 5 | return Calendar.current.date(byAdding: .init(day: -1), to: Date())! 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/ForecastController+Bridging.swift: -------------------------------------------------------------------------------- 1 | import ReactiveObjCBridge 2 | import ReactiveSwift 3 | import Result 4 | 5 | extension ForecastController { 6 | @objc(fetchWeatherUpdateForPlacemark:) 7 | // swiftlint:disable:next identifier_name 8 | public func __fetchWeatherUpdate(for placemark: CLPlacemark) -> RACSignal { 9 | return fetchWeatherUpdate(for: placemark).bridged 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/NSAttributedString.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension NSAttributedString { 4 | @objc var entireRange: NSRange { 5 | return NSRange(location: 0, length: length) 6 | } 7 | } 8 | 9 | extension NSAttributedString { 10 | @objc var font: UIFont? { 11 | return attribute(.font, at: 0, effectiveRange: nil) as? UIFont 12 | } 13 | 14 | @objc var textColor: UIColor? { 15 | return attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor 16 | } 17 | } 18 | 19 | extension NSMutableAttributedString { 20 | override var font: UIFont? { 21 | get { 22 | return super.font 23 | } 24 | set { 25 | if let font = newValue { 26 | setAttributes([.font: font], range: entireRange) 27 | } else { 28 | removeAttribute(.font, range: entireRange) 29 | } 30 | } 31 | } 32 | 33 | override var textColor: UIColor? { 34 | get { 35 | return super.textColor 36 | } 37 | set { 38 | if let color = newValue { 39 | setTextColor(color, forRange: entireRange) 40 | } else { 41 | removeAttribute(.foregroundColor, range: entireRange) 42 | } 43 | } 44 | } 45 | 46 | @objc func setTextColor(_ color: UIColor, forRange range: NSRange) { 47 | setAttributes([.foregroundColor: color], range: range) 48 | } 49 | 50 | @objc func setTextColor(_ color: UIColor, forSubstring substring: String) { 51 | let range = (string as NSString).range(of: substring) 52 | setTextColor(color, forRange: range) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/NSBundle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bundle { 4 | @objc static var troposBundle: Bundle { 5 | return Bundle(for: TroposBundleClass.self) 6 | } 7 | 8 | @objc var versionNumber: String? { 9 | return object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String 10 | } 11 | } 12 | 13 | private class TroposBundleClass: NSObject {} 14 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/NSFileCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSFileCoordinator { 4 | func coordinateReadingItem(at url: URL, byAccessor accessor: (URL) throws -> Result) throws -> Result { 5 | var accessorError: Error? 6 | var nsError: NSError? 7 | var result: Result? 8 | coordinate(readingItemAt: url, error: &nsError) { url in 9 | do { 10 | result = try accessor(url) 11 | } catch { 12 | accessorError = error 13 | } 14 | } 15 | 16 | switch (nsError as Error?, accessorError, result) { 17 | case let (error?, nil, nil), 18 | let (nil, error?, nil): 19 | throw error 20 | case let (nil, nil, result?): 21 | return result 22 | default: 23 | fatalError("expected either an error or result") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/NSLocalizedString.swift: -------------------------------------------------------------------------------- 1 | import func Foundation.NSLocalizedString 2 | 3 | func TroposCoreLocalizedString(_ string: String, comment: String = "") -> String { 4 | return NSLocalizedString(string, bundle: .troposBundle, comment: comment) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/UIColor.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | @objc public static var defaultTextColor: UIColor { 5 | return .white 6 | } 7 | 8 | @objc public static var lighterTextColor: UIColor { 9 | return UIColor(hue: 240.0 / 360.0, saturation: 0.02, brightness: 0.8, alpha: 1.0) 10 | } 11 | 12 | @objc public static var primaryBackgroundColor: UIColor { 13 | return UIColor(hue: 240.0 / 360.0, saturation: 0.24, brightness: 0.13, alpha: 1.0) 14 | } 15 | 16 | @objc public static var secondaryBackgroundColor: UIColor { 17 | return UIColor(hue: 240.0 / 360.0, saturation: 0.22, brightness: 0.16, alpha: 1.0) 18 | } 19 | 20 | @objc public static var hotColor: UIColor { 21 | return UIColor(hue: 11.0 / 360.0, saturation: 0.80, brightness: 0.92, alpha: 1.0) 22 | } 23 | 24 | @objc public static var warmerColor: UIColor { 25 | return UIColor(hue: 40.0 / 360.0, saturation: 1.0, brightness: 0.97, alpha: 1.0) 26 | } 27 | 28 | @objc public static var coolerColor: UIColor { 29 | return UIColor(hue: 194.0 / 360.0, saturation: 1.0, brightness: 0.93, alpha: 1.0) 30 | } 31 | 32 | @objc public static var coldColor: UIColor { 33 | return UIColor(hue: 194.0 / 360.0, saturation: 0.54, brightness: 0.95, alpha: 1.0) 34 | } 35 | 36 | @objc func lighten(by amount: CGFloat) -> UIColor { 37 | var hue: CGFloat = 0 38 | var saturation: CGFloat = 0 39 | var brightness: CGFloat = 0 40 | var alpha: CGFloat = 0 41 | 42 | getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) 43 | 44 | return UIColor( 45 | hue: hue, 46 | saturation: saturation * (1 - amount), 47 | brightness: brightness * (1 - amount) + amount, 48 | alpha: alpha 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/TroposCore/Extensions/UIFont.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | @objc public static func defaultLightFont(size: CGFloat) -> UIFont? { 5 | return UIFont(name: "DINNextLTPro-Light", size: size) 6 | } 7 | 8 | @objc public static func defaultRegularFont(size: CGFloat) -> UIFont? { 9 | return UIFont(name: "DINNextLTPro-Regular", size: size) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/TroposCore/Formatters/PrecipitationChanceFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PrecipitationChanceFormatter { 4 | func localizedStringFromPrecipitation(_ precipitation: Precipitation) -> String { 5 | let adjective = precipitation.chance.description.capitalized 6 | let type = precipitation.type.capitalized 7 | return TroposCoreLocalizedString(adjective + type) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/TroposCore/Formatters/RelativeDateFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct RelativeDateFormatter { 4 | private let calendar: Calendar 5 | private let dateFormatter: DateFormatter 6 | 7 | public init(calendar: Calendar = .current) { 8 | self.calendar = calendar 9 | self.dateFormatter = DateFormatter() 10 | dateFormatter.doesRelativeDateFormatting = true 11 | } 12 | 13 | public func localizedStringFromDate(_ date: Date) -> String { 14 | let timeString = timeStringFromDate(date) 15 | 16 | if let dateString = dateStringFromDate(date) { 17 | let format = TroposCoreLocalizedString("UpdatedAtDateAndTime") 18 | return String.localizedStringWithFormat(format, dateString, timeString) 19 | } else { 20 | let format = TroposCoreLocalizedString("UpdatedAtTime") 21 | return String.localizedStringWithFormat(format, timeString) 22 | } 23 | } 24 | 25 | private func dateStringFromDate(_ date: Date) -> String? { 26 | let components = calendar.dateComponents([.day, .hour, .minute, .second], from: date, to: Date()) 27 | guard components.day == 0 else { return nil } 28 | dateFormatter.dateStyle = .short 29 | dateFormatter.timeStyle = .none 30 | return dateFormatter.string(from: date) 31 | } 32 | 33 | private func timeStringFromDate(_ date: Date) -> String { 34 | dateFormatter.dateStyle = .none 35 | dateFormatter.timeStyle = .short 36 | return dateFormatter.string(from: date) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/TroposCore/Formatters/TemperatureComparisonFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct TemperatureComparisonFormatter { 4 | public init() {} 5 | 6 | public func localizedStrings( 7 | fromComparison comparison: TemperatureComparison, 8 | precipitation: String, 9 | date: Date 10 | ) -> (description: String, adjective: String) { 11 | let adjective = comparison.localizedAdjective 12 | let timeOfDay = Calendar.current.localizedTimeOfDay(forDate: date) 13 | let timeOfYesterday = Calendar.current.localizedTimeOfYesterday(relativeToDate: date) 14 | 15 | let format = comparison == .same 16 | ? TroposCoreLocalizedString("SameTemperatureFormat") 17 | : TroposCoreLocalizedString("DifferentTemperatureFormat") 18 | 19 | return ( 20 | description: String(format: format, adjective, timeOfDay, timeOfYesterday, precipitation), 21 | adjective: adjective 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TroposCore/Formatters/TemperatureFormatter.swift: -------------------------------------------------------------------------------- 1 | public struct TemperatureFormatter { 2 | public var unitSystem: UnitSystem 3 | 4 | public init(unitSystem: UnitSystem = SettingsController().unitSystem) { 5 | self.unitSystem = unitSystem 6 | } 7 | 8 | public func stringFromTemperature(_ temperature: Temperature) -> String { 9 | let usesMetricSystem = unitSystem == .metric 10 | let rawTemperature = usesMetricSystem ? temperature.celsiusValue : temperature.fahrenheitValue 11 | return String(format: "%.f°", Double(rawTemperature)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/TroposCore/Formatters/WindSpeedFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct WindSpeedFormatter { 4 | public var unitSystem: UnitSystem 5 | 6 | public init(unitSystem: UnitSystem = SettingsController().unitSystem) { 7 | self.unitSystem = unitSystem 8 | } 9 | 10 | public func localizedString(forWindSpeed windSpeed: Double, bearing: Double) -> String { 11 | guard let cardinalDirection = CardinalDirection(bearing: bearing) else { 12 | preconditionFailure("invalid bearing: \(bearing)") 13 | } 14 | 15 | let abbreviatedSpeedUnit: String 16 | let speed: Double 17 | 18 | if unitSystem == .metric { 19 | if Locale.current.regionCode == "CA" { 20 | abbreviatedSpeedUnit = "km/h" 21 | speed = kilometersPerHourFromMilesPerHour(windSpeed) 22 | } else { 23 | abbreviatedSpeedUnit = "m/s" 24 | speed = metersPerSecondFromMilesPerHour(windSpeed) 25 | } 26 | } else { 27 | abbreviatedSpeedUnit = "mph" 28 | speed = windSpeed 29 | } 30 | 31 | return String(format: "%.0f %@ %@", speed, abbreviatedSpeedUnit, cardinalDirection.localizedAbbreviation) 32 | } 33 | 34 | private func kilometersPerHourFromMilesPerHour(_ mph: Double) -> Double { 35 | return mph * 1.60934 36 | } 37 | 38 | private func metersPerSecondFromMilesPerHour(_ mph: Double) -> Double { 39 | return mph * 0.44704 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/TroposCore/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 214 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/CardinalDirection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum CardinalDirection: String { 4 | case north = "North" 5 | case northEast = "NorthEast" 6 | case east = "East" 7 | case southEast = "SouthEast" 8 | case south = "South" 9 | case southWest = "SouthWest" 10 | case west = "West" 11 | case northWest = "NorthWest" 12 | 13 | public init?(bearing: Double) { 14 | guard (0.0 ... 360.0).contains(bearing) else { return nil } 15 | 16 | if bearing < 22.5 { 17 | self = .north 18 | } else if bearing < 67.5 { 19 | self = .northEast 20 | } else if bearing < 112.5 { 21 | self = .east 22 | } else if bearing < 157.5 { 23 | self = .southEast 24 | } else if bearing < 202.5 { 25 | self = .south 26 | } else if bearing < 247.5 { 27 | self = .southWest 28 | } else if bearing < 292.5 { 29 | self = .west 30 | } else if bearing < 337.5 { 31 | self = .northWest 32 | } else { 33 | self = .north 34 | } 35 | } 36 | 37 | public var abbreviation: String { 38 | return rawValue.filter { "NSEW".contains($0) } 39 | } 40 | } 41 | 42 | public extension CardinalDirection { 43 | var localizedDescription: String { 44 | return TroposCoreLocalizedString(rawValue.capitalized) 45 | } 46 | 47 | var localizedAbbreviation: String { 48 | return TroposCoreLocalizedString(abbreviation) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/DailyForecast.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DailyForecast { 4 | public var date: Date 5 | public var conditionsDescription: String 6 | public var highTemperature: Temperature 7 | public var lowTemperature: Temperature 8 | 9 | public init?(json: [String: Any]?) { 10 | guard let json = json, 11 | let time = json["time"] as? Double, 12 | let icon = json["icon"] as? String, 13 | let temperatureMax = (json["temperatureMax"] as? NSNumber)?.intValue, 14 | let temperatureMin = (json["temperatureMin"] as? NSNumber)?.intValue 15 | else { 16 | return nil 17 | } 18 | 19 | self.date = Date(timeIntervalSince1970: time) 20 | self.conditionsDescription = icon 21 | self.highTemperature = Temperature(fahrenheitValue: temperatureMax) 22 | self.lowTemperature = Temperature(fahrenheitValue: temperatureMin) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/Precipitation.swift: -------------------------------------------------------------------------------- 1 | public struct Precipitation { 2 | public var probability: Double 3 | public var type: String 4 | 5 | public init(probability: Double, type: String) { 6 | self.probability = probability 7 | self.type = type 8 | } 9 | 10 | public var chance: PrecipitationChance { 11 | switch probability { 12 | case _ where probability > 0.3: return .good 13 | case _ where probability > 0: return .slight 14 | default: return .none 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/PrecipitationChance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PrecipitationChance: String, CustomStringConvertible { 4 | case none, slight, good 5 | 6 | public var description: String { 7 | return rawValue 8 | } 9 | 10 | public var localizedDescription: String { 11 | return TroposCoreLocalizedString(description) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/Temperature.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum TemperatureComparison: String, CustomStringConvertible { 4 | case same, hotter, warmer, cooler, colder 5 | 6 | public var description: String { 7 | return rawValue 8 | } 9 | 10 | public var localizedAdjective: String { 11 | return TroposCoreLocalizedString(description) 12 | } 13 | } 14 | 15 | private enum TemperatureLimit: Int { 16 | case hotter = 32 17 | case colder = 75 18 | } 19 | 20 | func - (lhs: Temperature, rhs: Temperature) -> Temperature { 21 | return Temperature(fahrenheitValue: lhs.fahrenheitValue - rhs.fahrenheitValue) 22 | } 23 | 24 | @objc(TRTemperature) public class Temperature: NSObject { 25 | @objc public private(set) lazy var fahrenheitValue: Int = { 26 | Int(round(Float(self.celsiusValue) * 9.0 / 5.0)) + 32 27 | }() 28 | 29 | @objc public private(set) lazy var celsiusValue: Int = { 30 | Int(round(Float(self.fahrenheitValue - 32) * 5.0 / 9.0)) 31 | }() 32 | 33 | @objc public init(fahrenheitValue: Int) { 34 | super.init() 35 | self.fahrenheitValue = fahrenheitValue 36 | } 37 | 38 | @objc public init(celsiusValue: Int) { 39 | super.init() 40 | self.celsiusValue = celsiusValue 41 | } 42 | 43 | @objc public func temperatureDifferenceFrom(_ temperature: Temperature) -> Temperature { 44 | return self - temperature 45 | } 46 | 47 | public func comparedTo(_ temperature: Temperature) -> TemperatureComparison { 48 | let diff = fahrenheitValue - temperature.fahrenheitValue 49 | switch diff { 50 | case _ where diff >= 10 && fahrenheitValue > TemperatureLimit.hotter.rawValue: return .hotter 51 | case _ where diff > 0: return .warmer 52 | case _ where diff == 0: return .same 53 | case _ where diff > -10 || fahrenheitValue > TemperatureLimit.colder.rawValue: return .cooler 54 | default: return .colder 55 | } 56 | } 57 | 58 | // MARK: NSObjectProtocol 59 | 60 | public override var description: String { 61 | return "Fahrenheit: \(fahrenheitValue)°\nCelsius: \(celsiusValue)°" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/TimeOfDay.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum TimeOfDay { 4 | case morning 5 | case day 6 | case afternoon 7 | case night 8 | } 9 | 10 | public extension Calendar { 11 | func timeOfDay(forDate date: Date) -> TimeOfDay { 12 | let components = dateComponents([.hour], from: date) 13 | 14 | if components.hour! < 4 { 15 | return .night 16 | } else if components.hour! < 9 { 17 | return .morning 18 | } else if components.hour! < 14 { 19 | return .day 20 | } else if components.hour! < 17 { 21 | return .afternoon 22 | } else { 23 | return .night 24 | } 25 | } 26 | 27 | func localizedTimeOfDay(forDate date: Date) -> String { 28 | switch timeOfDay(forDate: date) { 29 | case .night: 30 | return TroposCoreLocalizedString("Tonight") 31 | case .morning: 32 | return TroposCoreLocalizedString("ThisMorning") 33 | case .day: 34 | return TroposCoreLocalizedString("Today") 35 | case .afternoon: 36 | return TroposCoreLocalizedString("ThisAfternoon") 37 | } 38 | } 39 | 40 | func localizedTimeOfYesterday(relativeToDate date: Date) -> String { 41 | switch timeOfDay(forDate: date) { 42 | case .night: 43 | return TroposCoreLocalizedString("LastNight") 44 | case .morning: 45 | return TroposCoreLocalizedString("YesterdayMorning") 46 | case .day: 47 | return TroposCoreLocalizedString("Yesterday") 48 | case .afternoon: 49 | return TroposCoreLocalizedString("YesterdayAfternoon") 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/UnitSystem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum UnitSystem: Int { 4 | case metric 5 | case imperial 6 | } 7 | -------------------------------------------------------------------------------- /Sources/TroposCore/Models/WeatherUpdateCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | private let cacheQueue: OperationQueue = { 5 | let queue = OperationQueue() 6 | queue.maxConcurrentOperationCount = 1 7 | queue.name = "com.thoughtbot.carlweathers.CacheQueue" 8 | return queue 9 | }() 10 | 11 | @objc(TRWeatherUpdateCache) public final class WeatherUpdateCache: NSObject { 12 | @objc public static let latestWeatherUpdateFileName = "TRLatestWeatherUpdateFile" 13 | 14 | private let cacheURL: URL 15 | 16 | @objc public init(fileName: String, inDirectory directory: URL) { 17 | self.cacheURL = directory.appendingPathComponent(fileName) 18 | } 19 | 20 | @objc public convenience init?(fileName: String) { 21 | guard let cachesURL = FileManager.default 22 | .containerURL(forSecurityApplicationGroupIdentifier: "group.com.thoughtbot.carlweathers") 23 | else { return nil } 24 | self.init(fileName: fileName, inDirectory: cachesURL) 25 | } 26 | 27 | @objc public var latestWeatherUpdate: WeatherUpdate? { 28 | do { 29 | return try NSFileCoordinator(filePresenter: self).coordinateReadingItem(at: cacheURL) { cacheURL in 30 | NSKeyedUnarchiver.unarchiveObject(withFile: cacheURL.path) as? WeatherUpdate 31 | } 32 | } catch { 33 | if #available(iOS 10.0, iOSApplicationExtension 10.0, *) { 34 | os_log("Failed to read cached weather update: %{public}@", type: .error, error.localizedDescription) 35 | } else { 36 | NSLog("Failed to read cached weather update: %@", error.localizedDescription) 37 | } 38 | return nil 39 | } 40 | } 41 | 42 | @objc public func archiveWeatherUpdate( 43 | _ weatherUpdate: WeatherUpdate, 44 | completionHandler: @escaping (Bool, Error?) -> Void 45 | ) { 46 | let writingIntent = NSFileAccessIntent.writingIntent(with: cacheURL) 47 | NSFileCoordinator(filePresenter: self).coordinate(with: [writingIntent], queue: cacheQueue) { error in 48 | if let error = error { 49 | completionHandler(false, error) 50 | } else { 51 | let success = NSKeyedArchiver.archiveRootObject(weatherUpdate, toFile: writingIntent.url.path) 52 | completionHandler(success, nil) 53 | } 54 | } 55 | } 56 | } 57 | 58 | extension WeatherUpdateCache: NSFilePresenter { 59 | public var presentedItemURL: URL? { 60 | return cacheURL 61 | } 62 | 63 | public var presentedItemOperationQueue: OperationQueue { 64 | return cacheQueue 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/TroposCore/Protocols/Geocoder.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | public protocol Geocoder: class { 5 | func reverseGeocodeLocation( 6 | _ location: CLLocation, 7 | completionHandler: @escaping CoreLocation.CLGeocodeCompletionHandler 8 | ) 9 | 10 | func cancelGeocode() 11 | } 12 | 13 | extension CLGeocoder: Geocoder {} 14 | -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "29x29", 26 | "scale" : "3x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "40x40", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "57x57", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "iphone", 45 | "size" : "57x57", 46 | "scale" : "2x" 47 | }, 48 | { 49 | "idiom" : "iphone", 50 | "size" : "60x60", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "iphone", 55 | "size" : "60x60", 56 | "scale" : "3x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "20x20", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "20x20", 66 | "scale" : "2x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "29x29", 71 | "scale" : "1x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "29x29", 76 | "scale" : "2x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "40x40", 81 | "scale" : "1x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "40x40", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ipad", 90 | "size" : "50x50", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "idiom" : "ipad", 95 | "size" : "50x50", 96 | "scale" : "2x" 97 | }, 98 | { 99 | "idiom" : "ipad", 100 | "size" : "72x72", 101 | "scale" : "1x" 102 | }, 103 | { 104 | "idiom" : "ipad", 105 | "size" : "72x72", 106 | "scale" : "2x" 107 | }, 108 | { 109 | "idiom" : "ipad", 110 | "size" : "76x76", 111 | "scale" : "1x" 112 | }, 113 | { 114 | "idiom" : "ipad", 115 | "size" : "76x76", 116 | "scale" : "2x" 117 | }, 118 | { 119 | "idiom" : "ipad", 120 | "size" : "83.5x83.5", 121 | "scale" : "2x" 122 | }, 123 | { 124 | "idiom" : "ios-marketing", 125 | "size" : "1024x1024", 126 | "scale" : "1x" 127 | } 128 | ], 129 | "info" : { 130 | "version" : 1, 131 | "author" : "xcode" 132 | } 133 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/clear-day.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "clear-day.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/clear-day.imageset/clear-day.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/clear-day.imageset/clear-day.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/clear-night.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "clear-night.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/clear-night.imageset/clear-night.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/clear-night.imageset/clear-night.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/cloudy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cloudy.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/cloudy.imageset/cloudy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/cloudy.imageset/cloudy.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/fog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "fog.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/fog.imageset/fog.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/fog.imageset/fog.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/partly-cloudy-day.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "partly-cloudy-day.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/partly-cloudy-day.imageset/partly-cloudy-day.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/partly-cloudy-day.imageset/partly-cloudy-day.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/partly-cloudy-night.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "partly-cloudy-night.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/partly-cloudy-night.imageset/partly-cloudy-night.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/partly-cloudy-night.imageset/partly-cloudy-night.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/rain.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rain.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/rain.imageset/rain.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/rain.imageset/rain.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/sleet.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sleet.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/sleet.imageset/sleet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/sleet.imageset/sleet.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/snow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "snow.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/snow.imageset/snow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/snow.imageset/snow.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/wind.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "wind.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/Images.xcassets/Weather Icons/wind.imageset/wind.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/Resources/Images.xcassets/Weather Icons/wind.imageset/wind.pdf -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | // CardinalDirection.swift 2 | "North" = "North"; 3 | "Northeast" = "Northeast"; 4 | "East" = "East"; 5 | "Southeast" = "Southeast"; 6 | "South" = "South"; 7 | "Southwest" = "Southwest"; 8 | "West" = "West"; 9 | "Northwest" = "Northwest"; 10 | 11 | // PrecipitationChance.swift 12 | "Good" = "Good"; 13 | "Slight" = "Slight"; 14 | "None" = "None"; 15 | 16 | // PrecipitationChanceFormatter.swift 17 | "NoneRain" = ""; 18 | "SlightRain" = ", with a slight chance of rain"; 19 | "GoodRain" = ", with a good chance of rain"; 20 | "NoneSnow" = ", with no chance of snow"; 21 | "SlightSnow" = ", with a slight chance of snow"; 22 | "GoodSnow" = ", with a good chance of snow"; 23 | 24 | // RelativeDateFormatter.swift 25 | "UpdatedAtDateAndTime" = "Updated %@ at %@"; 26 | "UpdatedAtTime" = "Updated at %@"; 27 | 28 | // Temperature.swift 29 | "Hotter" = "hotter"; 30 | "Warmer" = "warmer"; 31 | "Cooler" = "cooler"; 32 | "Colder" = "colder"; 33 | "Same" = "same"; 34 | 35 | // TemperatureComparisonFormatter.swift 36 | "DifferentTemperatureFormat" = "It's %@ %@ than %@%@."; 37 | "PrecipitationChanceFormat" = "%@ chance of %@ %@."; 38 | "SameTemperatureFormat" = "It's the %@ %@ as %@%@."; 39 | 40 | // TimeOfDay.swift 41 | "Today" = "today"; 42 | "Yesterday" = "yesterday"; 43 | "Tonight" = "tonight"; 44 | "LastNight" = "last night"; 45 | "ThisMorning" = "this morning"; 46 | "YesterdayMorning" = "yesterday morning"; 47 | "ThisAfternoon" = "this afternoon"; 48 | "YesterdayAfternoon" = "yesterday afternoon"; 49 | -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | // CardinalDirection.swift 2 | "North" = "Nord"; 3 | "Northeast" = "Nord-Est"; 4 | "East" = "Est"; 5 | "Southeast" = "Sud-Est"; 6 | "South" = "Sud"; 7 | "Southwest" = "Sud-Ovest"; 8 | "West" = "Ovest"; 9 | "Northwest" = "Nord-Ovest"; 10 | 11 | // PrecipitationChance.swift 12 | "Good" = "Buona"; 13 | "Slight" = "Lieve"; 14 | "None" = "Nessuna"; 15 | 16 | // PrecipitationChanceFormatter.swift 17 | "NoneRain" = ""; 18 | "SlightRain" = ", con bassa probabilità di pioggia"; 19 | "GoodRain" = ", con alta probabilità di pioggia"; 20 | "NoneSnow" = ", nessuna possibilità di nevicate"; 21 | "SlightSnow" = ", con bassa probabilità di nevicate"; 22 | "GoodSnow" = ", con alta probabilità di nevicate"; 23 | 24 | // RelativeDateFormatter.swift 25 | "UpdatedAtDateAndTime" = "Aggiornato il %@ alle %@"; 26 | "UpdatedAtTime" = "Aggiornato il %@"; 27 | 28 | // Temperature.swift 29 | "Hotter" = "molto più caldo"; 30 | "Warmer" = "più caldo"; 31 | "Cooler" = "più freddo"; 32 | "Colder" = "molto più freddo"; 33 | "Same" = "è stabile"; 34 | 35 | // TemperatureComparisonFormatter.swift 36 | "DifferentTemperatureFormat" = "Fa %@ %@ rispetto %@%@."; 37 | "PrecipitationChanceFormat" = "%@ possibilità di %@ %@."; 38 | "SameTemperatureFormat" = "La temperatura %@ %@ rispetto a %@%@."; 39 | 40 | // TimeOfDay.swift 41 | "Today" = "oggi"; 42 | "Yesterday" = "ieri"; 43 | "Tonight" = "questa notte"; 44 | "LastNight" = "la scorsa notte"; 45 | "ThisMorning" = "questa mattina"; 46 | "YesterdayMorning" = "ieri mattina"; 47 | "ThisAfternoon" = "questo pomeriggio"; 48 | "YesterdayAfternoon" = "ieri pomeriggio"; 49 | -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/pl-PL.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | // CardinalDirection.swift 2 | "North" = "Północ"; 3 | "Northeast" = "Północny wschód"; 4 | "East" = "Wschód"; 5 | "Southeast" = "Południowy wschód"; 6 | "South" = "Południe"; 7 | "Southwest" = "Południowy zachód"; 8 | "West" = "Zachód"; 9 | "Northwest" = "Północny zachód"; 10 | 11 | // PrecipitationChance.swift 12 | "Good" = "Duża"; 13 | "Slight" = "Niewielka"; 14 | "None" = "Brak"; 15 | 16 | // PrecipitationChanceFormatter.swift 17 | "NoneRain" = ""; 18 | "SlightRain" = ", z małą szansą na deszcz"; 19 | "GoodRain" = ", z dużą szansą na deszcz"; 20 | "NoneSnow" = " bez śniegu"; 21 | "SlightSnow" = ", z małą szansą na śnieg"; 22 | "GoodSnow" = ", z dużą szansą na śnieg"; 23 | 24 | // RelativeDateFormatter.swift 25 | "UpdatedAtDateAndTime" = "Zaktualizowano %@ o %@"; 26 | "UpdatedAtTime" = "Zaktualizowano o %@"; 27 | 28 | // Temperature.swift 29 | "Hotter" = "goręcej"; 30 | "Warmer" = "cieplej"; 31 | "Cooler" = "chłodniej"; 32 | "Colder" = "zimniej"; 33 | "Same" = "tak samo"; 34 | 35 | // TemperatureComparisonFormatter.swift 36 | "DifferentTemperatureFormat" = "%2$@ jest %1$@ niż %3$@%4$@."; 37 | "PrecipitationChanceFormat" = "%@ szansy na %@ %@."; 38 | "SameTemperatureFormat" = "%2$@ jest %1$@ jak %3$@%4$@."; 39 | 40 | // TimeOfDay.swift 41 | "Today" = "dziś"; 42 | "Yesterday" = "wczoraj"; 43 | "Tonight" = "dziś wieczorem"; 44 | "LastNight" = "zeszłej nocy"; 45 | "ThisMorning" = "dziś rano"; 46 | "YesterdayMorning" = "wczoraj rano"; 47 | "ThisAfternoon" = "dziś po południu"; 48 | "YesterdayAfternoon" = "wczoraj po południu"; 49 | -------------------------------------------------------------------------------- /Sources/TroposCore/Resources/sv.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | // CardinalDirection.swift 2 | "North" = "Norr"; 3 | "Northeast" = "Nordöst"; 4 | "East" = "Öst"; 5 | "Southeast" = "Sydöst"; 6 | "South" = "Syd"; 7 | "Southwest" = "Sydväst"; 8 | "West" = "Väst"; 9 | "Northwest" = "Nordväst"; 10 | 11 | // PrecipitationChance.swift 12 | /* These keys are translated so that they can be used in the context of weather prognosis followed by the word for "chance" - "chans" (SWE). */ 13 | "Good" = "Stor"; 14 | "Slight" = "Liten"; 15 | "None" = "Ingen"; 16 | 17 | // PrecipitationChanceFormatter.swift 18 | "NoneRain" = ""; 19 | "SlightRain" = ", med liten chans för regn"; 20 | "GoodRain" = ", med stor chans för regn"; 21 | "NoneSnow" = ", utan chans för snö"; 22 | "SlightSnow" = ", med liten chans för snö"; 23 | "GoodSnow" = ", med stor chans för snö"; 24 | 25 | // RelativeDateFormatter.swift 26 | "UpdatedAtDateAndTime" = "Uppdaterat %@ vid %@"; 27 | "UpdatedAtTime" = "Uppdaterat vid %@"; 28 | 29 | // Temperature.swift 30 | "Hotter" = "hetare"; 31 | "Warmer" = "varmare"; 32 | "Cooler" = "svalare"; 33 | "Colder" = "kallare"; 34 | /* The word temperature is added at the end since the word is needed to make the sentence understandable in this context */ 35 | "Same" = "samma temperatur"; 36 | 37 | // TemperatureComparisonFormatter.swift 38 | "DifferentTemperatureFormat" = "Det är %@ %@ än %@%@."; 39 | "PrecipitationChanceFormat" = "%@ chans för %@ %@."; 40 | "SameTemperatureFormat" = "Det är %@ %@ som %@%@."; 41 | 42 | // TimeOfDay.swift 43 | "Today" = "idag"; 44 | "Yesterday" = "igår"; 45 | "Tonight" = "ikväll"; 46 | "LastNight" = "igår kväll"; 47 | "ThisMorning" = "denna morgon"; 48 | "YesterdayMorning" = "igår morse"; 49 | "ThisAfternoon" = "denna eftermiddag"; 50 | "YesterdayAfternoon" = "igår eftermiddag"; 51 | -------------------------------------------------------------------------------- /Sources/TroposCore/TRErrors.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | static NSString * const TRErrorDomain = @"TroposErrorDomain"; 4 | 5 | typedef NS_ENUM(NSInteger, TRError) { 6 | TRErrorLocationUnauthorized, 7 | 8 | TRErrorConditionsResponseFailed = 200, 9 | TRErrorConditionsResponseLocationNotFound, 10 | TRErrorConditionsResponseUnexpectedFormat, 11 | }; 12 | -------------------------------------------------------------------------------- /Sources/TroposCore/TroposCore.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double TroposCoreVersionNumber; 4 | FOUNDATION_EXPORT const unsigned char TroposCoreVersionString[]; 5 | 6 | FOUNDATION_EXPORT NSString *const TRSettingsUnitSystemKey; 7 | FOUNDATION_EXPORT NSString *const TRSettingsLastVersionKey; 8 | 9 | #import 10 | -------------------------------------------------------------------------------- /Sources/TroposCore/TroposCore.m: -------------------------------------------------------------------------------- 1 | #import "TroposCore.h" 2 | 3 | NSString *const TRSettingsUnitSystemKey = @"TRUnitSystem"; 4 | NSString *const TRSettingsLastVersionKey = @"TRLastVersion"; 5 | -------------------------------------------------------------------------------- /Sources/TroposCore/ViewModels/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Sources/TroposCore/ViewModels/.gitkeep -------------------------------------------------------------------------------- /Sources/TroposCore/ViewModels/DailyForecastViewModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @objc(TRDailyForecastViewModel) public final class DailyForecastViewModel: NSObject { 4 | fileprivate let dailyForecast: DailyForecast 5 | fileprivate let temperatureFormatter: TemperatureFormatter 6 | 7 | public init(dailyForecast: DailyForecast, temperatureFormatter: TemperatureFormatter) { 8 | self.dailyForecast = dailyForecast 9 | self.temperatureFormatter = temperatureFormatter 10 | } 11 | 12 | public convenience init(dailyForecast: DailyForecast) { 13 | self.init(dailyForecast: dailyForecast, temperatureFormatter: TemperatureFormatter()) 14 | } 15 | } 16 | 17 | public extension DailyForecastViewModel { 18 | @objc var dayOfWeek: String { 19 | let formatter = DateFormatter() 20 | formatter.dateFormat = "ccc" 21 | return formatter.string(from: dailyForecast.date) 22 | } 23 | 24 | @objc var conditionsImage: UIImage? { 25 | return UIImage(named: dailyForecast.conditionsDescription, in: .troposBundle, compatibleWith: nil) 26 | } 27 | 28 | @objc var highTemperature: String { 29 | return temperatureFormatter.stringFromTemperature(dailyForecast.highTemperature) 30 | } 31 | 32 | @objc var lowTemperature: String { 33 | return temperatureFormatter.stringFromTemperature(dailyForecast.lowTemperature) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TroposIntents/Base.lproj/Intents.intentdefinition: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INEnums 6 | 7 | INIntentDefinitionModelVersion 8 | 1.0 9 | INIntentDefinitionSystemVersion 10 | 17G65 11 | INIntentDefinitionToolsBuildVersion 12 | 10L232m 13 | INIntentDefinitionToolsVersion 14 | 10.0 15 | INIntents 16 | 17 | 18 | INIntentCategory 19 | information 20 | INIntentClassName 21 | CheckWeatherIntent 22 | INIntentClassPrefix 23 | TR 24 | INIntentDescription 25 | Check the current weather forecast 26 | INIntentDescriptionID 27 | RwwC7B 28 | INIntentLastParameterTag 29 | 0 30 | INIntentName 31 | CheckWeather 32 | INIntentParameterCombinations 33 | 34 | 35 | 36 | INIntentParameterCombinationIsPrimary 37 | 38 | INIntentParameterCombinationSubtitle 39 | 40 | INIntentParameterCombinationSubtitleID 41 | 4KVOKA 42 | INIntentParameterCombinationSupportsBackgroundExecution 43 | 44 | INIntentParameterCombinationTitle 45 | 46 | INIntentParameterCombinationTitleID 47 | n36P4y 48 | 49 | 50 | INIntentParameters 51 | 52 | INIntentResponse 53 | 54 | INIntentResponseCodes 55 | 56 | 57 | INIntentResponseCodeFormatString 58 | 59 | INIntentResponseCodeFormatStringID 60 | Jw1nTQ 61 | INIntentResponseCodeName 62 | failure 63 | INIntentResponseCodeSuccess 64 | 65 | 66 | 67 | INIntentResponseCodeFormatString 68 | ${conditionsDescription} 69 | INIntentResponseCodeFormatStringID 70 | Hpkcrr 71 | INIntentResponseCodeName 72 | success 73 | INIntentResponseCodeSuccess 74 | 75 | 76 | 77 | INIntentResponseLastParameterTag 78 | 1 79 | INIntentResponseParameters 80 | 81 | 82 | INIntentResponseParameterDisplayPriority 83 | 1 84 | INIntentResponseParameterName 85 | conditionsDescription 86 | INIntentResponseParameterSupportsMultipleValues 87 | 88 | INIntentResponseParameterTag 89 | 1 90 | INIntentResponseParameterType 91 | String 92 | 93 | 94 | 95 | INIntentRestrictions 96 | 0 97 | INIntentTitle 98 | Check weather forecast 99 | INIntentTitleID 100 | 8SKsHj 101 | INIntentType 102 | Custom 103 | INIntentUserConfirmationRequired 104 | 105 | INIntentVerb 106 | View 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Sources/TroposIntents/CheckWeatherIntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | import ReactiveSwift 4 | import Result 5 | import TroposCore 6 | 7 | @available(iOS 12.0, *) 8 | public final class CheckWeatherIntentHandler: NSObject, CheckWeatherIntentHandling { 9 | public func handle(intent: CheckWeatherIntent, completion: @escaping (CheckWeatherIntentResponse) -> Void) { 10 | DispatchQueue.main.async { 11 | self.updateWeather(completion: completion) 12 | } 13 | } 14 | 15 | func updateWeather(completion: @escaping (CheckWeatherIntentResponse) -> Void) { 16 | let forecast = ForecastController(apiKey: TRForecastAPIKey) 17 | let geocode = GeocodeController() 18 | let location = LocationController() 19 | 20 | let weatherUpdate = location.requestAuthorization() 21 | .flatMap(.latest) { _ in location.requestLocation() } 22 | .flatMap(.latest) { location in geocode.reverseGeocode(location) } 23 | .mapError(AnyError.init) 24 | .flatMap(.latest) { placemark in forecast.fetchWeatherUpdate(for: placemark) } 25 | 26 | weatherUpdate.startWithResult { result in 27 | switch result { 28 | case let .success(weatherUpdate): 29 | self.cacheWeatherUpdate(weatherUpdate) { 30 | let viewModel = WeatherViewModel(weatherUpdate: weatherUpdate) 31 | completion(.success(conditionsDescription: viewModel.conditionsDescription.string)) 32 | } 33 | case .failure: 34 | completion(.init(code: .failure, userActivity: nil)) 35 | } 36 | } 37 | } 38 | 39 | func cacheWeatherUpdate(_ weatherUpdate: WeatherUpdate, completion: @escaping () -> Void) { 40 | guard let cache = WeatherUpdateCache(fileName: WeatherUpdateCache.latestWeatherUpdateFileName) else { 41 | completion() 42 | return 43 | } 44 | 45 | cache.archiveWeatherUpdate(weatherUpdate) { isSuccess, error in 46 | defer { completion() } 47 | if isSuccess { return } 48 | 49 | if let error = error { 50 | os_log("Failed to archive weather update: %{public}@", type: .error, error.localizedDescription) 51 | } else { 52 | os_log("Failed to archive weather update", type: .error) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/TroposIntents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | TroposIntents 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.2.1 21 | CFBundleVersion 22 | 214 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | IntentsRestrictedWhileLocked 28 | 29 | IntentsRestrictedWhileProtectedDataUnavailable 30 | 31 | IntentsSupported 32 | 33 | CheckWeatherIntent 34 | 35 | 36 | NSExtensionPointIdentifier 37 | com.apple.intents-service 38 | NSExtensionPrincipalClass 39 | $(PRODUCT_MODULE_NAME).IntentHandler 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Sources/TroposIntents/IntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | import TroposCore 3 | 4 | final class IntentHandler: INExtension { 5 | override func handler(for intent: INIntent) -> Any { 6 | guard intent is CheckWeatherIntent else { 7 | preconditionFailure("Unexpected intent type: \(intent)") 8 | } 9 | return CheckWeatherIntentHandler() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/TroposIntents/TroposIntents-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "Secrets.h" 2 | -------------------------------------------------------------------------------- /Sources/TroposIntents/TroposIntents.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.thoughtbot.carlweathers 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/TroposIntents/it.lproj/Intents.strings: -------------------------------------------------------------------------------- 1 | "8SKsHj" = "Controlla previsioni meteo"; 2 | 3 | "Hpkcrr" = "${conditionsDescription}"; 4 | 5 | "RwwC7B" = "Controlla le attuali previsioni meteo"; 6 | -------------------------------------------------------------------------------- /Sources/TroposIntents/pl-PL.lproj/Intents.strings: -------------------------------------------------------------------------------- 1 | "8SKsHj" = "Sprawdź prognozę pogody"; 2 | 3 | "Hpkcrr" = "${conditionsDescription}"; 4 | 5 | "RwwC7B" = "Sprawdź aktualną prognozę pogody"; 6 | -------------------------------------------------------------------------------- /Sources/TroposIntents/sv.lproj/Intents.strings: -------------------------------------------------------------------------------- 1 | "8SKsHj" = "Kontrollera väderprognosen"; 2 | 3 | "Hpkcrr" = "${conditionsDescription}"; 4 | 5 | "RwwC7B" = "Kontrollera aktuell väderprognos"; 6 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Tests/TroposCoreTests/.gitkeep -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Controllers/ForecastControllerSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import OHHTTPStubs 3 | import Quick 4 | import TroposCore 5 | 6 | final class ForecastControllerSpec: QuickSpec { 7 | override func spec() { 8 | describe("fetchWeatherUpdate") { 9 | beforeEach(handleUnexpectedNetworkRequests) 10 | afterEach(OHHTTPStubs.removeAllStubs) 11 | 12 | it("fetches weather from api.forecast.io") { 13 | expect { 14 | ForecastController(apiKey: "test").fetchWeatherUpdate(for: testPlacemark) 15 | }.toEventually( 16 | makeNetworkRequest(matching: .scheme("https") && .host("api.forecast.io")) 17 | ) 18 | } 19 | 20 | it("requests today and yesterday's forecasts with the correct API key and location") { 21 | expect { 22 | ForecastController(apiKey: "correct-key").fetchWeatherUpdate(for: testPlacemark) 23 | }.toEventually( 24 | satisfyAllOf( 25 | makeNetworkRequest(matching: .path("/forecast/correct-key/40.759069,-73.984961")), 26 | makeNetworkRequest(matching: .path(regex: "^/forecast/correct-key/40.759069,-73.984961,")) 27 | ) 28 | ) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Controllers/GeocodeControllerSpec.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Nimble 3 | import Quick 4 | import ReactiveSwift 5 | @testable import TroposCore 6 | 7 | final class GeocodeControllerSpec: QuickSpec { 8 | override func spec() { 9 | describe("reverseGeocode") { 10 | it("sends the geocoded place and completes") { 11 | let geocoder = TestGeocoder(name: "test place") 12 | let controller = GeocodeController(geocoder: geocoder) 13 | let location = CLLocation(latitude: 1, longitude: 2) 14 | 15 | var isComplete = false 16 | var error: CLError? 17 | var place: CLPlacemark? 18 | 19 | controller.reverseGeocode(location).start { event in 20 | switch event { 21 | case let .value(value): 22 | place = value 23 | case let .failed(err): 24 | error = err 25 | case .completed: 26 | isComplete = true 27 | case .interrupted: 28 | break 29 | } 30 | } 31 | 32 | expect(isComplete).toEventually(beTrue()) 33 | expect(place?.location?.coordinate).to(equal(location.coordinate)) 34 | expect(place?.name).to(equal("test place")) 35 | expect(error).to(beNil()) 36 | } 37 | 38 | it("passes through an error if it occurs") { 39 | let geocoder = TestGeocoder(error: .geocodeFoundNoResult) 40 | let controller = GeocodeController(geocoder: geocoder) 41 | let location = CLLocation(latitude: -1, longitude: -2) 42 | 43 | var error: CLError? 44 | controller.reverseGeocode(location).startWithFailed { error = $0 } 45 | 46 | expect(error).toEventuallyNot(beNil()) 47 | } 48 | 49 | it("cancels the underlying geocode if the subscription is cancelled") { 50 | let geocoder = TestGeocoder(name: "test place") 51 | let controller = GeocodeController(geocoder: geocoder) 52 | let location = CLLocation(latitude: 3, longitude: 4) 53 | 54 | var value: CLPlacemark? 55 | var error: Error? 56 | var isCompleted = false 57 | var isInterrupted = false 58 | 59 | controller.reverseGeocode(location).start { event in 60 | switch event { 61 | case let .value(val): 62 | value = val 63 | case let .failed(err): 64 | error = err 65 | case .completed: 66 | isCompleted = true 67 | case .interrupted: 68 | isInterrupted = true 69 | } 70 | } 71 | 72 | geocoder.cancelGeocode() 73 | 74 | expect(isInterrupted).toEventually(beTrue()) 75 | expect(isCompleted).to(beFalse()) 76 | expect(error).to(beNil()) 77 | expect(value).to(beNil()) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Controllers/SettingsControllerSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import TroposCore 4 | 5 | final class SettingsControllerSpec: QuickSpec { 6 | override func spec() { 7 | describe("TRSettingsController") { 8 | describe("registered defaults") { 9 | it("returns the correct value in an imperial locale") { 10 | let defaults = UserDefaults.makeRandomDefaults() 11 | let controller = SettingsController(locale: Locale(identifier: "en_US"), userDefaults: defaults) 12 | controller.registerSettings() 13 | expect(controller.unitSystem) == UnitSystem.imperial 14 | } 15 | 16 | it("returns the correct value in a metric locale") { 17 | let defaults = UserDefaults.makeRandomDefaults() 18 | let controller = SettingsController(locale: Locale(identifier: "en_AU"), userDefaults: defaults) 19 | controller.registerSettings() 20 | expect(controller.unitSystem) == UnitSystem.metric 21 | } 22 | } 23 | 24 | describe("migration") { 25 | it("converts from a string unit system representation to an integer") { 26 | UserDefaults.standard.set("1", forKey: TRSettingsUnitSystemKey) 27 | let controller = SettingsController() 28 | expect(controller.unitSystem) == UnitSystem.imperial 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Extensions/TRTemperatureColorsSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | @testable import TroposCore 4 | 5 | final class TRTemperatureColorsSpec: QuickSpec { 6 | override func spec() { 7 | describe("UIColor+TRTemperatureColors") { 8 | describe("lighterColorByAmount") { 9 | it("blends with white") { 10 | let warmer = UIColor.warmerColor 11 | 12 | let blendedColor = warmer.lighten(by: 0.5) 13 | 14 | var hue: CGFloat = 0 15 | var saturation: CGFloat = 0 16 | var brightness: CGFloat = 0 17 | var alpha: CGFloat = 0 18 | blendedColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) 19 | 20 | expect(saturation).to(equal(0.5)) 21 | expect(brightness).to(beCloseTo(0.985, within: 0.001)) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Formatters/TemperatureComparisonFormatterSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import TroposCore 4 | 5 | private func dateFromString(_ string: String) -> Date { 6 | let formatter = DateFormatter() 7 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" 8 | return formatter.date(from: string)! 9 | } 10 | 11 | final class TemperatureComparisonFormatterSpec: QuickSpec { 12 | override func spec() { 13 | describe("TRTemperatureComparisonFormatter") { 14 | describe("localizedStringFromComparison:adjective:precipitation") { 15 | it("bases the time of day off of the date") { 16 | let previousTimeZone = NSTimeZone.default 17 | NSTimeZone.default = TimeZone(abbreviation: "UTC")! 18 | defer { NSTimeZone.default = previousTimeZone } 19 | 20 | let date = dateFromString("2015-05-15 22:00:00 UTC") 21 | let (description, _) = TemperatureComparisonFormatter() 22 | .localizedStrings(fromComparison: .same, precipitation: "", date: date) 23 | expect(description) == "It's the same tonight as last night." 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Formatters/TemperatureFormatterSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import TroposCore 4 | 5 | final class TemperatureFormatterSpec: QuickSpec { 6 | override func spec() { 7 | it("can format fahrenheit temperatures in celsius") { 8 | let formatter = TemperatureFormatter(unitSystem: .metric) 9 | let temperature = Temperature(fahrenheitValue: 32) 10 | expect(formatter.stringFromTemperature(temperature)) == "0°" 11 | } 12 | 13 | it("can format celsius temperatures in fahrenheit") { 14 | let formatter = TemperatureFormatter(unitSystem: .imperial) 15 | let temperature = Temperature(celsiusValue: 0) 16 | expect(formatter.stringFromTemperature(temperature)) == "32°" 17 | } 18 | 19 | it("it fetches the unit system from the SettingsController by default") { 20 | SettingsController().unitSystem = .metric 21 | let formatter = TemperatureFormatter() 22 | let temperature = Temperature(fahrenheitValue: 32) 23 | expect(formatter.stringFromTemperature(temperature)) == "0°" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 214 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Models/CardinalDirectionSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import TroposCore 4 | 5 | final class CardinalDirectionSpec: QuickSpec { 6 | override func spec() { 7 | it("abbreviates each cardinal direction correctly") { 8 | expect(CardinalDirection.north.abbreviation) == "N" 9 | expect(CardinalDirection.south.abbreviation) == "S" 10 | expect(CardinalDirection.east.abbreviation) == "E" 11 | expect(CardinalDirection.west.abbreviation) == "W" 12 | expect(CardinalDirection.northEast.abbreviation) == "NE" 13 | expect(CardinalDirection.southEast.abbreviation) == "SE" 14 | expect(CardinalDirection.northWest.abbreviation) == "NW" 15 | expect(CardinalDirection.southWest.abbreviation) == "SW" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Models/PrecipitationSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | import Quick 4 | import TroposCore 5 | 6 | class PrecipitationSpec: QuickSpec { 7 | override func spec() { 8 | describe("Precipitation") { 9 | context("precipitation chance") { 10 | it("returns none for 0% chance of precipitation") { 11 | let precipitation = Precipitation(probability: 0.0, type: "") 12 | expect(precipitation.chance).to(equal(PrecipitationChance.none)) 13 | } 14 | 15 | it("returns slight for 1 - 30% chance of precipitation") { 16 | let precipitation = Precipitation(probability: 0.2, type: "") 17 | expect(precipitation.chance).to(equal(PrecipitationChance.slight)) 18 | } 19 | 20 | it("returns good for chance greater than 30% of precipitation") { 21 | let precipitation = Precipitation(probability: 0.7, type: "") 22 | expect(precipitation.chance).to(equal(PrecipitationChance.good)) 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Models/TemperatureSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import TroposCore 4 | 5 | class TemperatureSpec: QuickSpec { 6 | override func spec() { 7 | describe("Temperature") { 8 | func compare(_ temp1: Int, _ temp2: Int) -> TemperatureComparison { 9 | let t1 = Temperature(fahrenheitValue: temp1) 10 | let t2 = Temperature(fahrenheitValue: temp2) 11 | return t1.comparedTo(t2) 12 | } 13 | 14 | context("temperature is 10 less than the receiver") { 15 | it("is above freezing and returns hotter") { 16 | expect(compare(80, 70)).to(equal(TemperatureComparison.hotter)) 17 | } 18 | 19 | it("is below freezing and returns warmer") { 20 | expect(compare(21, 10)).to(equal(TemperatureComparison.warmer)) 21 | } 22 | } 23 | 24 | context("temperature is withing 10 less of the receiver") { 25 | it("returns warmer") { 26 | expect(compare(9, 0)).to(equal(TemperatureComparison.warmer)) 27 | } 28 | } 29 | 30 | context("temperature is within 10 greater of the receiver") { 31 | it("returns cooler") { 32 | expect(compare(0, 9)).to(equal(TemperatureComparison.cooler)) 33 | } 34 | } 35 | 36 | context("temperature is 10 greater than the receiver") { 37 | it("temperature is above 75 and returns cooler") { 38 | expect(compare(85, 95)).to(equal(TemperatureComparison.cooler)) 39 | } 40 | 41 | it("temperature is below 75 and returns colder") { 42 | expect(compare(0, 10)).to(equal(TemperatureComparison.colder)) 43 | } 44 | } 45 | 46 | context("temperatures are the same") { 47 | it("returns same") { 48 | expect(compare(0, 0)).to(equal(TemperatureComparison.same)) 49 | } 50 | } 51 | 52 | context("Conversion to celsius should return correct values") { 53 | it("should convert 75 to 24") { 54 | expect(Temperature(fahrenheitValue: 75).celsiusValue).to(equal(24)) 55 | } 56 | 57 | it("should convert 50 to 10") { 58 | expect(Temperature(fahrenheitValue: 50).celsiusValue).to(equal(10)) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Resources/New York.placemark: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Tests/TroposCoreTests/Resources/New York.placemark -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/CoreLocation.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | extension CLLocationCoordinate2D: Equatable { 4 | public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 5 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/NSDate+ISO8601.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | init?(iso8601String: String) { 5 | let formatter = DateFormatter() 6 | formatter.dateFormat = "yyyy-MM-dd" 7 | guard let date = formatter.date(from: iso8601String) else { return nil } 8 | let timeInterval = date.timeIntervalSince(Date(timeIntervalSince1970: 0)) 9 | self.init(timeIntervalSince1970: timeInterval) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/OHHTTPStubs+Matchers.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import OHHTTPStubs 3 | import Quick 4 | import ReactiveSwift 5 | 6 | private let isAnyRequest: OHHTTPStubsTestBlock = { _ in true } 7 | 8 | func handleUnexpectedNetworkRequests(_ metadata: ExampleMetadata) { 9 | stub(condition: isAnyRequest) { request in 10 | let callsite = metadata.example.callsite 11 | fail("unexpected request: \(request)", file: callsite.file, line: callsite.line) 12 | return OHHTTPStubsResponse(error: TestError.unexpectedRequest) 13 | } 14 | } 15 | 16 | func makeNetworkRequest(matching condition: StubCondition) -> Predicate { 17 | var disposable: Disposable? 18 | var isMatch = false 19 | 20 | stub(condition: condition.condition) { _ in 21 | isMatch = true 22 | return OHHTTPStubsResponse() 23 | } 24 | 25 | var currentResult: PredicateResult { 26 | return PredicateResult( 27 | status: isMatch ? .matches : .fail, 28 | message: .fail("expected to fire network request matching condition '\(condition.description)'") 29 | ) 30 | } 31 | 32 | return Predicate.define { expression in 33 | if disposable == nil { 34 | let producer = try expression.evaluate()!.producer 35 | disposable = ScopedDisposable(producer.start()) 36 | } 37 | 38 | return currentResult 39 | }.requireNonNil 40 | } 41 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/StubCondition.swift: -------------------------------------------------------------------------------- 1 | import OHHTTPStubs 2 | 3 | struct StubCondition { 4 | let condition: OHHTTPStubsTestBlock 5 | let description: String 6 | 7 | static func && (lhs: StubCondition, rhs: StubCondition) -> StubCondition { 8 | return StubCondition( 9 | condition: lhs.condition && rhs.condition, 10 | description: "\(lhs.description), \(rhs.description)" 11 | ) 12 | } 13 | } 14 | 15 | extension StubCondition { 16 | static func host(_ host: String) -> StubCondition { 17 | return StubCondition(condition: isHost(host), description: "host=\(host)") 18 | } 19 | 20 | static func path(_ path: String) -> StubCondition { 21 | return StubCondition(condition: isPath(path), description: "path=\(path)") 22 | } 23 | 24 | static func path(regex pattern: String) -> StubCondition { 25 | return StubCondition(condition: pathMatches(pattern), description: "path~=\(pattern)") 26 | } 27 | 28 | static func scheme(_ scheme: String) -> StubCondition { 29 | return StubCondition(condition: isScheme(scheme), description: "scheme=\(scheme)") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/TestError.swift: -------------------------------------------------------------------------------- 1 | enum TestError: Error { 2 | case unexpectedRequest 3 | } 4 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/TestGeocoder.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | import MapKit 4 | import Result 5 | @testable import TroposCore 6 | 7 | final class TestGeocoder: Geocoder { 8 | private enum State { 9 | case ready 10 | case geocoding 11 | case cancelled 12 | } 13 | 14 | private var geocodeState = State.ready 15 | private let result: Result 16 | 17 | init(name: String) { 18 | self.result = .success(name) 19 | } 20 | 21 | init(error code: CLError.Code) { 22 | let error = NSError(domain: CLError.errorDomain, code: code.rawValue) as! CLError 23 | result = .failure(error) 24 | } 25 | 26 | func reverseGeocodeLocation(_ location: CLLocation, completionHandler: @escaping CLGeocodeCompletionHandler) { 27 | precondition(geocodeState == .ready) 28 | geocodeState = .geocoding 29 | 30 | DispatchQueue.main.async { 31 | defer { self.geocodeState = .ready } 32 | 33 | guard self.geocodeState != .cancelled else { 34 | let error = NSError(domain: CLError.errorDomain, code: CLError.geocodeCanceled.rawValue) as! CLError 35 | completionHandler(nil, error) 36 | return 37 | } 38 | 39 | switch self.result { 40 | case let .success(name): 41 | let placemark = MKPlacemark(coordinate: location.coordinate, addressDictionary: ["Name": name]) 42 | completionHandler([placemark], nil) 43 | case let .failure(error): 44 | completionHandler(nil, error) 45 | } 46 | } 47 | } 48 | 49 | func cancelGeocode() { 50 | if geocodeState == .geocoding { 51 | geocodeState = .cancelled 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/TestPlacemark.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | var testPlacemark: CLPlacemark { 4 | let path = Bundle(for: WeatherUpdateSpec.self).path(forResource: "New York", ofType: "placemark")! 5 | return NSKeyedUnarchiver.unarchiveObject(withFile: path) as! CLPlacemark 6 | } 7 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/Support/UserDefaults+Random.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UserDefaults { 4 | static func makeRandomDefaults() -> UserDefaults { 5 | return UserDefaults(suiteName: UUID().description)! 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/TroposCoreTests/ViewModels/DailyForecastViewModelSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | @testable import TroposCore 4 | 5 | final class DailyForecastViewModelSpec: QuickSpec { 6 | override func spec() { 7 | var testForecast: DailyForecast { 8 | return DailyForecast(json: [ 9 | "time": Date(iso8601String: "2016-04-18")!.timeIntervalSince1970, 10 | "icon": "clear-day", 11 | "temperatureMin": 50, 12 | "temperatureMax": 75, 13 | ])! 14 | } 15 | 16 | it("formats the day of week") { 17 | let viewModel = DailyForecastViewModel(dailyForecast: testForecast) 18 | expect(viewModel.dayOfWeek) == "Mon" 19 | } 20 | 21 | it("returns the expected icon image") { 22 | let viewModel = DailyForecastViewModel(dailyForecast: testForecast) 23 | expect(viewModel.conditionsImage) == UIImage(named: "clear-day", in: .troposBundle, compatibleWith: nil) 24 | } 25 | 26 | it("returns the formatted high temperature") { 27 | let formatter = TemperatureFormatter(unitSystem: .imperial) 28 | let viewModel = DailyForecastViewModel(dailyForecast: testForecast, temperatureFormatter: formatter) 29 | expect(viewModel.highTemperature) == "75°" 30 | } 31 | 32 | it("returns the formatted low temperature") { 33 | let formatter = TemperatureFormatter(unitSystem: .imperial) 34 | let viewModel = DailyForecastViewModel(dailyForecast: testForecast, temperatureFormatter: formatter) 35 | expect(viewModel.lowTemperature) == "50°" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/TroposTests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Tests/TroposTests/.gitkeep -------------------------------------------------------------------------------- /Tests/TroposTests/Controllers/ApplicationControllerSpec.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import UIKit 4 | 5 | final class ApplicationControllerSpec: QuickSpec { 6 | override func spec() { 7 | describe("setMinimimBackgroundFetchIntervalForApplication") { 8 | it("sets the interval to minimum when authorization is always") { 9 | let application = TestApplication() 10 | let locationController = TestLocationController() 11 | locationController.authorizationStatus = .authorizedAlways 12 | let controller = ApplicationController(locationController: locationController) 13 | 14 | var interval: TimeInterval? 15 | application.didSetMinimumBackgroundFetchInterval = { interval = $0 } 16 | controller.setMinimumBackgroundFetchInterval(for: application) 17 | 18 | expect(interval).to(equal(UIApplicationBackgroundFetchIntervalMinimum)) 19 | } 20 | 21 | it("sets the interval to never when authorization is not always") { 22 | let application = TestApplication() 23 | let locationController = TestLocationController() 24 | locationController.authorizationStatus = .authorizedWhenInUse 25 | let controller = ApplicationController(locationController: locationController) 26 | 27 | var interval: TimeInterval? 28 | application.didSetMinimumBackgroundFetchInterval = { interval = $0 } 29 | controller.setMinimumBackgroundFetchInterval(for: application) 30 | 31 | expect(interval).to(equal(UIApplicationBackgroundFetchIntervalNever)) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/TroposTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.2.1 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 214 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/TroposTests/Support/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/Tropos/0e1c06c28f6546da7e3930f60a82fc797a687086/Tests/TroposTests/Support/.gitkeep -------------------------------------------------------------------------------- /Tests/TroposTests/Support/TestApplication.swift: -------------------------------------------------------------------------------- 1 | final class TestApplication: NSObject, TRApplication { 2 | var didSetMinimumBackgroundFetchInterval: ((TimeInterval) -> Void)? 3 | 4 | func setMinimumBackgroundFetchInterval(_ minimumBackgroundFetchInterval: TimeInterval) { 5 | didSetMinimumBackgroundFetchInterval?(minimumBackgroundFetchInterval) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/TroposTests/Support/TestLocationController.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import TroposCore 3 | 4 | class TestLocationController: LocationController { 5 | var authorizationStatus: CLAuthorizationStatus = .notDetermined 6 | 7 | override func authorizationStatusEqualTo(_ status: CLAuthorizationStatus) -> Bool { 8 | return authorizationStatus == status 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/TroposTests/Support/TroposTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "TRApplicationController.h" 2 | -------------------------------------------------------------------------------- /Tropos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tropos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /bin/archive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eo pipefail 4 | 5 | xcpretty="$(ruby -r rubygems -e 'puts Gem.bindir(Gem.user_dir)')"/xcpretty 6 | 7 | if [ "$CI" = "true" ] && [ -z "$DISABLE_CODE_SIGNING" ]; then 8 | DISABLE_CODE_SIGNING="true" 9 | fi 10 | 11 | if [ "$DISABLE_CODE_SIGNING" = "true" ]; then 12 | echo >&2 "Running with code signing disabled (set DISABLE_CODE_SIGNING=false to enable)" 13 | 14 | code_sign_xcconfig=$(mktemp /tmp/tropos-bin-archive.XXXXXX) 15 | # shellcheck disable=SC2064 16 | trap "rm -f '$code_sign_xcconfig'" INT TERM HUP EXIT 17 | cat >"$code_sign_xcconfig" <&2 "Running with code signing enabled (set DISABLE_CODE_SIGNING=true to disable)" 26 | fi 27 | 28 | xcodebuild archive \ 29 | -project Tropos.xcodeproj \ 30 | -scheme Tropos \ 31 | -archivePath "$PWD/build/Tropos.xcarchive" \ 32 | SYMROOT="$PWD/build" \ 33 | | "$xcpretty" -c 34 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | gem_install_log="${GEM_INSTALL_LOG_PATH:-.gem-install.log}" 6 | 7 | gem_install() { 8 | gem_name=$1 9 | 10 | gem install "$gem_name" --user-install &> "$gem_install_log" \ 11 | && echo "Installed $gem_name" 12 | } 13 | 14 | if [[ -n $CARTHAGE_LOG_PATH ]]; then 15 | carthage_log_opts=(--log-path "$CARTHAGE_LOG_PATH") 16 | fi 17 | 18 | brew bundle 19 | gem_install xcpretty 20 | carthage bootstrap --platform iOS --cache-builds --no-use-binaries "${carthage_log_opts[@]}" 21 | 22 | tropos_secrets="Config/Secrets.h" 23 | example_secrets="Config/Secrets-Example.h" 24 | 25 | if [ ! -f "${tropos_secrets}" ]; then 26 | cp "${example_secrets}" "${tropos_secrets}" 27 | echo "" 28 | echo "--------------------------------------------------------------------------------" 29 | echo "Created ${tropos_secrets}. Please add your keys to it." 30 | echo "--------------------------------------------------------------------------------" 31 | fi 32 | -------------------------------------------------------------------------------- /bin/swiftlint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if command -v swiftlint; then 6 | swiftlint --config "${SRCROOT}"/.swiftlint.yml "$@" 7 | else 8 | echo "warning: run bin/setup to install SwiftLint" 9 | fi 10 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec fastlane test "$@" 4 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec carthage update --platform iOS --cache-builds "$@" 4 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "com.thoughtbot.carlweathers" 2 | apple_id "ios@thoughtbot.com" 3 | team_id "8E7TQ638ZD" 4 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # vim: ft=ruby 2 | 3 | fastlane_require "dotenv" 4 | Dotenv.load("../.env") 5 | 6 | fastlane_version "1.89.0" 7 | 8 | default_platform :ios 9 | 10 | platform :ios do 11 | desc "Runs all the tests" 12 | lane :test do 13 | run_tests(scheme: "Tropos") 14 | end 15 | 16 | desc "Submit a new Beta Build to Apple TestFlight" 17 | desc "This will also make sure the profile is up to date" 18 | lane :beta do |options| 19 | setup_certs 20 | 21 | next_build_number(options) do 22 | build 23 | testflight(skip_waiting_for_build_processing: true) 24 | end 25 | end 26 | 27 | desc "Deploy a new version to the App Store" 28 | lane :release do |options| 29 | setup_certs 30 | 31 | next_build_number(options) do 32 | build 33 | deliver(force: true) 34 | end 35 | end 36 | 37 | desc "Bump the version, tag and push" 38 | lane :bump_version do |options| 39 | next_build_number(options) 40 | end 41 | 42 | private_lane :build do 43 | build_ios_app(scheme: "Tropos", buildlog_path: "log") 44 | end 45 | 46 | private_lane :setup_certs do 47 | validate_env! 48 | match(type: "appstore", app_identifier: [ 49 | "com.thoughtbot.carlweathers", 50 | "com.thoughtbot.carlweathers.TroposIntents", 51 | ]) 52 | end 53 | end 54 | 55 | def next_build_number(options={}) 56 | type = options.fetch(:type, "build") 57 | 58 | case type 59 | when "build" 60 | increment_build_number 61 | else 62 | increment_version_number(bump_type: type) 63 | increment_build_number 64 | end 65 | 66 | result = yield if block_given? 67 | 68 | commit_version_bump(xcodeproj: "Tropos.xcodeproj", ignore: %r{^(?!Sources)/}) 69 | version = get_version_number(target: "Tropos") 70 | build = get_build_number 71 | add_git_tag(tag: "v#{version}+#{build}") 72 | push_to_git_remote 73 | 74 | result 75 | end 76 | 77 | def validate_env! 78 | username = ensure_deliver_username 79 | match_url = ensure_match_url 80 | if !username || !match_url 81 | offer_to_write_env_file 82 | end 83 | end 84 | 85 | def ensure_deliver_username 86 | username = ENV["DELIVER_USERNAME"] 87 | if !username || username.empty? 88 | ENV["DELIVER_USERNAME"] = UI.input("Please provide your Apple ID username:") 89 | ensure_deliver_username 90 | end 91 | username 92 | end 93 | 94 | def ensure_match_url 95 | match_url = ENV["MATCH_GIT_URL"] 96 | if !match_url || match_url.empty? 97 | ENV["MATCH_GIT_URL"] = UI.input("Please provide the URL to your match repo:") 98 | ensure_match_url 99 | end 100 | match_url 101 | end 102 | 103 | def offer_to_write_env_file 104 | return unless UI.interactive? 105 | 106 | if UI.confirm("Some environment variables were missing. "\ 107 | "Would you like to write them to a .env file?") 108 | File.open("../.env", "w") do |file| 109 | file.puts("DELIVER_USERNAME=#{ENV["DELIVER_USERNAME"]}") 110 | file.puts("MATCH_GIT_URL=#{ENV["MATCH_GIT_URL"]}") 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /fastlane/Gymfile: -------------------------------------------------------------------------------- 1 | output_directory "./build" 2 | -------------------------------------------------------------------------------- /fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url "https://github.com/thoughtbot/certificates.git" 2 | type "development" 3 | username "ios@thoughtbot.com" 4 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios test 20 | ``` 21 | fastlane ios test 22 | ``` 23 | Runs all the tests 24 | ### ios beta 25 | ``` 26 | fastlane ios beta 27 | ``` 28 | Submit a new Beta Build to Apple TestFlight 29 | 30 | This will also make sure the profile is up to date 31 | ### ios release 32 | ``` 33 | fastlane ios release 34 | ``` 35 | Deploy a new version to the App Store 36 | ### ios bump_version 37 | ``` 38 | fastlane ios bump_version 39 | ``` 40 | Bump the version, tag and push 41 | 42 | ---- 43 | 44 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 45 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 46 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 47 | --------------------------------------------------------------------------------