diff --git a/.gitignore b/.gitignore index 2c22487..7dd787f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,65 +1,224 @@ -# Xcode +######################### +# .gitignore file for Xcode4 and Xcode5 Source projects # -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +# Apple bugs, waiting for Apple to fix/respond: +# +# 15564624 - what does the xccheckout file in Xcode5 do? Where's the documentation? +# +# Version 2.6 +# For latest version, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects +# +# 2015 updates: +# - Fixed typo in "xccheckout" line - thanks to @lyck for pointing it out! +# - Fixed the .idea optional ignore. Thanks to @hashier for pointing this out +# - Finally added "xccheckout" to the ignore. Apple still refuses to answer support requests about this, but in practice it seems you should ignore it. +# - minor tweaks from Jona and Coeur (slightly more precise xc* filtering/names) +# 2014 updates: +# - appended non-standard items DISABLED by default (uncomment if you use those tools) +# - removed the edit that an SO.com moderator made without bothering to ask me +# - researched CocoaPods .lock more carefully, thanks to Gokhan Celiker +# 2013 updates: +# - fixed the broken "save personal Schemes" +# - added line-by-line explanations for EVERYTHING (some were missing) +# +# NB: if you are storing "built" products, this WILL NOT WORK, +# and you should use a different .gitignore (or none at all) +# This file is for SOURCE projects, where there are many extra +# files that we want to exclude +# +######################### + +##### +# Create your own Config.plist according to ConfigExample.plist + +Config.plist + +##### +# OS X temporary files that should never be committed +# +# c.f. http://www.westwind.com/reference/os-x/invisibles.html + +.DS_Store + +# c.f. http://www.westwind.com/reference/os-x/invisibles.html + +.Trashes + +# c.f. http://www.westwind.com/reference/os-x/invisibles.html + +*.swp + +# +# *.lock - this is used and abused by many editors for many different things. +# For the main ones I use (e.g. Eclipse), it should be excluded +# from source-control, but YMMV. +# (lock files are usually local-only file-synchronization on the local FS that should NOT go in git) +# c.f. the "OPTIONAL" section at bottom though, for tool-specific variations! +# +# In particular, if you're using CocoaPods, you'll want to comment-out this line: +*.lock + + +# +# profile - REMOVED temporarily (on double-checking, I can't find it in OS X docs?) +#profile + + +#### +# Xcode temporary files that should never be committed +# +# NB: NIB/XIB files still exist even on Storyboard projects, so we want this... + +*~.nib + + +#### +# Xcode build files - +# +# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" -## Build generated -build/ DerivedData/ -## Various settings +# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" + +build/ + + +##### +# Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) +# +# This is complicated: +# +# SOMETIMES you need to put this file in version control. +# Apple designed it poorly - if you use "custom executables", they are +# saved in this file. +# 99% of projects do NOT use those, so they do NOT want to version control this file. +# ..but if you're in the 1%, comment out the line "*.pbxuser" + +# .pbxuser: http://lists.apple.com/archives/xcode-users/2004/Jan/msg00193.html + *.pbxuser -!default.pbxuser + +# .mode1v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html + *.mode1v3 -!default.mode1v3 + +# .mode2v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html + *.mode2v3 -!default.mode2v3 + +# .perspectivev3: http://stackoverflow.com/questions/5223297/xcode-projects-what-is-a-perspectivev3-file + *.perspectivev3 + +# NB: also, whitelist the default ones, some projects need to use these +!default.pbxuser +!default.mode1v3 +!default.mode2v3 !default.perspectivev3 -xcuserdata/ -## Other -*.moved-aside -*.xcuserstate -## Obj-C/Swift specific -*.hmap -*.ipa -*.dSYM.zip -*.dSYM +#### +# Xcode 4 - semi-personal settings +# +# Apple Shared data that Apple put in the wrong folder +# c.f. http://stackoverflow.com/a/19260712/153422 +# FROM ANSWER: Apple says "don't ignore it" +# FROM COMMENTS: Apple is wrong; Apple code is too buggy to trust; there are no known negative side-effects to ignoring Apple's unofficial advice and instead doing the thing that actively fixes bugs in Xcode +# Up to you, but ... current advice: ignore it. +*.xccheckout -## Playgrounds -timeline.xctimeline -playground.xcworkspace +# +# +# OPTION 1: --------------------------------- +# throw away ALL personal settings (including custom schemes! +# - unless they are "shared") +# As per build/ and DerivedData/, this ought to have a trailing slash +# +# NB: this is exclusive with OPTION 2 below +xcuserdata/ + +# OPTION 2: --------------------------------- +# get rid of ALL personal settings, but KEEP SOME OF THEM +# - NB: you must manually uncomment the bits you want to keep +# +# NB: this *requires* git v1.8.2 or above; you may need to upgrade to latest OS X, +# or manually install git over the top of the OS X version +# NB: this is exclusive with OPTION 1 above +# +#xcuserdata/**/* -# Swift Package Manager +# (requires option 2 above): Personal Schemes # -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -.build/ +#!xcuserdata/**/xcschemes/* -# CocoaPods +#### +# XCode 4 workspaces - more detailed +# +# Workspaces are important! They are a core feature of Xcode - don't exclude them :) +# +# Workspace layout is quite spammy. For reference: +# +# /(root)/ +# /(project-name).xcodeproj/ +# project.pbxproj +# /project.xcworkspace/ +# contents.xcworkspacedata +# /xcuserdata/ +# /(your name)/xcuserdatad/ +# UserInterfaceState.xcuserstate +# /xcshareddata/ +# /xcschemes/ +# (shared scheme name).xcscheme +# /xcuserdata/ +# /(your name)/xcuserdatad/ +# (private scheme).xcscheme +# xcschememanagement.plist # -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -# Pods/ -# Carthage +#### +# Xcode 4 - Deprecated classes +# +# Allegedly, if you manually "deprecate" your classes, they get moved here. # -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts +# We're using source-control, so this is a "feature" that we do not want! -Carthage/Build +*.moved-aside + +#### +# OPTIONAL: Some well-known tools that people use side-by-side with Xcode / iOS development +# +# NB: I'd rather not include these here, but gitignore's design is weak and doesn't allow +# modular gitignore: you have to put EVERYTHING in one file. +# +# COCOAPODS: +# +# c.f. http://guides.cocoapods.org/using/using-cocoapods.html#what-is-a-podfilelock +# c.f. http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control +# +!Podfile.lock +!Manifest.lock +# +# RUBY: +# +# c.f. http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/ +# +#!Gemfile.lock +# +# IDEA: +# +# c.f. https://www.jetbrains.com/objc/help/managing-projects-under-version-control.html?search=workspace.xml +# +#.idea/workspace.xml +# +# TEXTMATE: +# +# -- UNVERIFIED: c.f. http://stackoverflow.com/a/50283/153422 +# +#tm_build_errors -# fastlane +#### +# UNKNOWN: recommended by others, but I can't discover what these files are # -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output diff --git a/AutolabsTask/AutolabsTask.xcodeproj/project.pbxproj b/AutolabsTask/AutolabsTask.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2620920 --- /dev/null +++ b/AutolabsTask/AutolabsTask.xcodeproj/project.pbxproj @@ -0,0 +1,556 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 144E15B4210753DB0012884A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15B3210753DB0012884A /* AppDelegate.swift */; }; + 144E15BB210753DE0012884A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 144E15BA210753DE0012884A /* Assets.xcassets */; }; + 144E15BE210753DE0012884A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 144E15BC210753DE0012884A /* LaunchScreen.storyboard */; }; + 144E15C9210753DE0012884A /* AutolabsTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15C8210753DE0012884A /* AutolabsTaskTests.swift */; }; + 144E15D4210754F10012884A /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15D3210754F10012884A /* Error.swift */; }; + 144E15D7210755220012884A /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15D6210755220012884A /* Weather.swift */; }; + 144E15DA2107553F0012884A /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15D92107553F0012884A /* Result.swift */; }; + 144E15DF210758010012884A /* WeatherByCoordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15DE210758010012884A /* WeatherByCoordinate.swift */; }; + 144E15E1210758150012884A /* WeatherByCityName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15E0210758150012884A /* WeatherByCityName.swift */; }; + 144E15E3210758400012884A /* OpenWeatherMapWebService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15E2210758400012884A /* OpenWeatherMapWebService.swift */; }; + 144E15E5210758BC0012884A /* Config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 144E15E4210758BC0012884A /* Config.plist */; }; + 144E15E7210758C80012884A /* ConfigExample.plist in Resources */ = {isa = PBXBuildFile; fileRef = 144E15E6210758C80012884A /* ConfigExample.plist */; }; + 144E15E9210759280012884A /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15E8210759280012884A /* Config.swift */; }; + 144E15F621076FFA0012884A /* WebService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15F521076FFA0012884A /* WebService.swift */; }; + 144E15FA210772B40012884A /* SpeechProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15F9210772B40012884A /* SpeechProcessor.swift */; }; + 144E15FC210773170012884A /* UserSpeechProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15FB210773170012884A /* UserSpeechProcessor.swift */; }; + 144E1600210798990012884A /* LocationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E15FF210798990012884A /* LocationProvider.swift */; }; + 144E1602210798E20012884A /* UserLocationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144E1601210798E20012884A /* UserLocationProvider.swift */; }; + 14D7AFE321079E7D00AA1836 /* WeatherUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D7AFE221079E7D00AA1836 /* WeatherUseCase.swift */; }; + 14D7AFE52107E66500AA1836 /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D7AFE42107E66500AA1836 /* WeatherViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 144E15C5210753DE0012884A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 144E15A8210753DB0012884A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 144E15AF210753DB0012884A; + remoteInfo = AutolabsTask; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 144E15B0210753DB0012884A /* AutolabsTask.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AutolabsTask.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 144E15B3210753DB0012884A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 144E15BA210753DE0012884A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 144E15BD210753DE0012884A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 144E15BF210753DE0012884A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 144E15C4210753DE0012884A /* AutolabsTaskTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutolabsTaskTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 144E15C8210753DE0012884A /* AutolabsTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutolabsTaskTests.swift; sourceTree = ""; }; + 144E15CA210753DE0012884A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 144E15D3210754F10012884A /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + 144E15D6210755220012884A /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; + 144E15D92107553F0012884A /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; + 144E15DE210758010012884A /* WeatherByCoordinate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherByCoordinate.swift; sourceTree = ""; }; + 144E15E0210758150012884A /* WeatherByCityName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherByCityName.swift; sourceTree = ""; }; + 144E15E2210758400012884A /* OpenWeatherMapWebService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenWeatherMapWebService.swift; sourceTree = ""; }; + 144E15E4210758BC0012884A /* Config.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Config.plist; sourceTree = ""; }; + 144E15E6210758C80012884A /* ConfigExample.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ConfigExample.plist; sourceTree = ""; }; + 144E15E8210759280012884A /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 144E15F521076FFA0012884A /* WebService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebService.swift; sourceTree = ""; }; + 144E15F9210772B40012884A /* SpeechProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechProcessor.swift; sourceTree = ""; }; + 144E15FB210773170012884A /* UserSpeechProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSpeechProcessor.swift; sourceTree = ""; }; + 144E15FF210798990012884A /* LocationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationProvider.swift; sourceTree = ""; }; + 144E1601210798E20012884A /* UserLocationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocationProvider.swift; sourceTree = ""; }; + 14D7AFE221079E7D00AA1836 /* WeatherUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherUseCase.swift; sourceTree = ""; }; + 14D7AFE42107E66500AA1836 /* WeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 144E15AD210753DB0012884A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 144E15C1210753DE0012884A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 144E15A7210753DB0012884A = { + isa = PBXGroup; + children = ( + 144E15B2210753DB0012884A /* AutolabsTask */, + 144E15C7210753DE0012884A /* AutolabsTaskTests */, + 144E15B1210753DB0012884A /* Products */, + ); + sourceTree = ""; + }; + 144E15B1210753DB0012884A /* Products */ = { + isa = PBXGroup; + children = ( + 144E15B0210753DB0012884A /* AutolabsTask.app */, + 144E15C4210753DE0012884A /* AutolabsTaskTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 144E15B2210753DB0012884A /* AutolabsTask */ = { + isa = PBXGroup; + children = ( + 144E15E4210758BC0012884A /* Config.plist */, + 144E15E6210758C80012884A /* ConfigExample.plist */, + 144E15E8210759280012884A /* Config.swift */, + 144E15B3210753DB0012884A /* AppDelegate.swift */, + 144E15D3210754F10012884A /* Error.swift */, + 144E15D92107553F0012884A /* Result.swift */, + 14D7AFE221079E7D00AA1836 /* WeatherUseCase.swift */, + 14D7AFE72107F5D100AA1836 /* Location */, + 14D7AFE62107F5BD00AA1836 /* Speech */, + 144E15D5210755170012884A /* Models */, + 144E15DB210755550012884A /* Networking */, + 14DDB0A92108A1690099AE03 /* ViewControllers */, + 144E15BA210753DE0012884A /* Assets.xcassets */, + 144E15BC210753DE0012884A /* LaunchScreen.storyboard */, + 144E15BF210753DE0012884A /* Info.plist */, + ); + path = AutolabsTask; + sourceTree = ""; + }; + 144E15C7210753DE0012884A /* AutolabsTaskTests */ = { + isa = PBXGroup; + children = ( + 144E15C8210753DE0012884A /* AutolabsTaskTests.swift */, + 144E15CA210753DE0012884A /* Info.plist */, + ); + path = AutolabsTaskTests; + sourceTree = ""; + }; + 144E15D5210755170012884A /* Models */ = { + isa = PBXGroup; + children = ( + 144E15D6210755220012884A /* Weather.swift */, + ); + path = Models; + sourceTree = ""; + }; + 144E15DB210755550012884A /* Networking */ = { + isa = PBXGroup; + children = ( + 144E15F521076FFA0012884A /* WebService.swift */, + 144E15E2210758400012884A /* OpenWeatherMapWebService.swift */, + 144E15DE210758010012884A /* WeatherByCoordinate.swift */, + 144E15E0210758150012884A /* WeatherByCityName.swift */, + ); + path = Networking; + sourceTree = ""; + }; + 14D7AFE62107F5BD00AA1836 /* Speech */ = { + isa = PBXGroup; + children = ( + 144E15F9210772B40012884A /* SpeechProcessor.swift */, + 144E15FB210773170012884A /* UserSpeechProcessor.swift */, + ); + path = Speech; + sourceTree = ""; + }; + 14D7AFE72107F5D100AA1836 /* Location */ = { + isa = PBXGroup; + children = ( + 144E15FF210798990012884A /* LocationProvider.swift */, + 144E1601210798E20012884A /* UserLocationProvider.swift */, + ); + path = Location; + sourceTree = ""; + }; + 14DDB0A92108A1690099AE03 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + 14D7AFE42107E66500AA1836 /* WeatherViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 144E15AF210753DB0012884A /* AutolabsTask */ = { + isa = PBXNativeTarget; + buildConfigurationList = 144E15CD210753DE0012884A /* Build configuration list for PBXNativeTarget "AutolabsTask" */; + buildPhases = ( + 144E15AC210753DB0012884A /* Sources */, + 144E15AD210753DB0012884A /* Frameworks */, + 144E15AE210753DB0012884A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AutolabsTask; + productName = AutolabsTask; + productReference = 144E15B0210753DB0012884A /* AutolabsTask.app */; + productType = "com.apple.product-type.application"; + }; + 144E15C3210753DE0012884A /* AutolabsTaskTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 144E15D0210753DE0012884A /* Build configuration list for PBXNativeTarget "AutolabsTaskTests" */; + buildPhases = ( + 144E15C0210753DE0012884A /* Sources */, + 144E15C1210753DE0012884A /* Frameworks */, + 144E15C2210753DE0012884A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 144E15C6210753DE0012884A /* PBXTargetDependency */, + ); + name = AutolabsTaskTests; + productName = AutolabsTaskTests; + productReference = 144E15C4210753DE0012884A /* AutolabsTaskTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 144E15A8210753DB0012884A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0940; + LastUpgradeCheck = 0940; + ORGANIZATIONNAME = "Rab Gábor"; + TargetAttributes = { + 144E15AF210753DB0012884A = { + CreatedOnToolsVersion = 9.4.1; + }; + 144E15C3210753DE0012884A = { + CreatedOnToolsVersion = 9.4.1; + TestTargetID = 144E15AF210753DB0012884A; + }; + }; + }; + buildConfigurationList = 144E15AB210753DB0012884A /* Build configuration list for PBXProject "AutolabsTask" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 144E15A7210753DB0012884A; + productRefGroup = 144E15B1210753DB0012884A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 144E15AF210753DB0012884A /* AutolabsTask */, + 144E15C3210753DE0012884A /* AutolabsTaskTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 144E15AE210753DB0012884A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 144E15E5210758BC0012884A /* Config.plist in Resources */, + 144E15BE210753DE0012884A /* LaunchScreen.storyboard in Resources */, + 144E15E7210758C80012884A /* ConfigExample.plist in Resources */, + 144E15BB210753DE0012884A /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 144E15C2210753DE0012884A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 144E15AC210753DB0012884A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 144E15F621076FFA0012884A /* WebService.swift in Sources */, + 14D7AFE52107E66500AA1836 /* WeatherViewController.swift in Sources */, + 144E15E3210758400012884A /* OpenWeatherMapWebService.swift in Sources */, + 144E15D7210755220012884A /* Weather.swift in Sources */, + 144E1602210798E20012884A /* UserLocationProvider.swift in Sources */, + 144E15DA2107553F0012884A /* Result.swift in Sources */, + 144E15FA210772B40012884A /* SpeechProcessor.swift in Sources */, + 144E15FC210773170012884A /* UserSpeechProcessor.swift in Sources */, + 14D7AFE321079E7D00AA1836 /* WeatherUseCase.swift in Sources */, + 144E15D4210754F10012884A /* Error.swift in Sources */, + 144E15DF210758010012884A /* WeatherByCoordinate.swift in Sources */, + 144E15B4210753DB0012884A /* AppDelegate.swift in Sources */, + 144E15E9210759280012884A /* Config.swift in Sources */, + 144E15E1210758150012884A /* WeatherByCityName.swift in Sources */, + 144E1600210798990012884A /* LocationProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 144E15C0210753DE0012884A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 144E15C9210753DE0012884A /* AutolabsTaskTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 144E15C6210753DE0012884A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 144E15AF210753DB0012884A /* AutolabsTask */; + targetProxy = 144E15C5210753DE0012884A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 144E15BC210753DE0012884A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 144E15BD210753DE0012884A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 144E15CB210753DE0012884A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 144E15CC210753DE0012884A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 144E15CE210753DE0012884A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Y8ZHVBUF4Q; + INFOPLIST_FILE = AutolabsTask/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.rabgabor.AutolabsTask; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 144E15CF210753DE0012884A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Y8ZHVBUF4Q; + INFOPLIST_FILE = AutolabsTask/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.rabgabor.AutolabsTask; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 144E15D1210753DE0012884A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Y8ZHVBUF4Q; + INFOPLIST_FILE = AutolabsTaskTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.rabgabor.AutolabsTaskTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutolabsTask.app/AutolabsTask"; + }; + name = Debug; + }; + 144E15D2210753DE0012884A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Y8ZHVBUF4Q; + INFOPLIST_FILE = AutolabsTaskTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.rabgabor.AutolabsTaskTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutolabsTask.app/AutolabsTask"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 144E15AB210753DB0012884A /* Build configuration list for PBXProject "AutolabsTask" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 144E15CB210753DE0012884A /* Debug */, + 144E15CC210753DE0012884A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 144E15CD210753DE0012884A /* Build configuration list for PBXNativeTarget "AutolabsTask" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 144E15CE210753DE0012884A /* Debug */, + 144E15CF210753DE0012884A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 144E15D0210753DE0012884A /* Build configuration list for PBXNativeTarget "AutolabsTaskTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 144E15D1210753DE0012884A /* Debug */, + 144E15D2210753DE0012884A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 144E15A8210753DB0012884A /* Project object */; +} diff --git a/AutolabsTask/AutolabsTask.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AutolabsTask/AutolabsTask.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ae8a0d0 --- /dev/null +++ b/AutolabsTask/AutolabsTask.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AutolabsTask/AutolabsTask.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AutolabsTask/AutolabsTask.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/AutolabsTask/AutolabsTask.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/AutolabsTask/AutolabsTask/AppDelegate.swift b/AutolabsTask/AutolabsTask/AppDelegate.swift new file mode 100644 index 0000000..339cc93 --- /dev/null +++ b/AutolabsTask/AutolabsTask/AppDelegate.swift @@ -0,0 +1,51 @@ +// +// AppDelegate.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + let speechProcessor = UserSpeechProcessor() + let locationProvider = UserLocationProvider() + var weatherUseCase: WeatherUseCase! // To prevent ARC from releasing it + + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { + let config: Config + do { + config = try Config() + } catch { + print(error.localizedDescription) + preconditionFailure(error.localizedDescription) + } + + let weatherViewController = WeatherViewController() + + weatherUseCase = WeatherUseCase(viewController: weatherViewController, + webService: OpenWeatherMapWebService(apiKey: config.apiKey), + speechProcessor: speechProcessor, + locationProvider: locationProvider) + + self.window = UIWindow(frame: UIScreen.main.bounds) + self.window?.rootViewController = weatherViewController + self.window?.makeKeyAndVisible() + + return true + } + + func applicationDidEnterBackground(_ application: UIApplication) { + speechProcessor.stopSpeechRecognition() + } + + func applicationWillEnterForeground(_ application: UIApplication) { + speechProcessor.startSpeechRecognition() + locationProvider.refresh() + } +} diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/AppIcon.appiconset/Contents.json b/AutolabsTask/AutolabsTask/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/Contents.json b/AutolabsTask/AutolabsTask/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/Contents.json b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny1.imageset/Contents.json b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny1.imageset/Contents.json new file mode 100644 index 0000000..3c0b426 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sunny1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny1.imageset/sunny1@2x.png b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny1.imageset/sunny1@2x.png new file mode 100644 index 0000000..943599b Binary files /dev/null and b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny1.imageset/sunny1@2x.png differ diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny2.imageset/Contents.json b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny2.imageset/Contents.json new file mode 100644 index 0000000..59cfb03 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sunny2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny2.imageset/sunny2@2x.png b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny2.imageset/sunny2@2x.png new file mode 100644 index 0000000..ee290a0 Binary files /dev/null and b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny2.imageset/sunny2@2x.png differ diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny3.imageset/Contents.json b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny3.imageset/Contents.json new file mode 100644 index 0000000..0a88fb9 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sunny3@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny3.imageset/sunny3@2x.png b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny3.imageset/sunny3@2x.png new file mode 100644 index 0000000..ebc64a7 Binary files /dev/null and b/AutolabsTask/AutolabsTask/Assets.xcassets/sunny/sunny3.imageset/sunny3@2x.png differ diff --git a/AutolabsTask/AutolabsTask/Base.lproj/LaunchScreen.storyboard b/AutolabsTask/AutolabsTask/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..5a863b5 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AutolabsTask/AutolabsTask/Config.swift b/AutolabsTask/AutolabsTask/Config.swift new file mode 100644 index 0000000..211e222 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Config.swift @@ -0,0 +1,43 @@ +// +// Config.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +struct Config { + + let apiKey: String + + init(fileName: String = "Config.plist") throws { + guard let path = Bundle.main.path(forResource: fileName, ofType: nil) else { + let errorMessage = + "\n------------------------------- Warning! -------------------------------\n" + + "\(fileName) not found. Please, create it according to ConfigExample.plist\n" + + "and provide your own OpenWeatherMap API key.\n" + + "------------------------------------------------------------------------\n" + + throw AutolabsError(code: .configFileNotFound, + message: errorMessage, + cause: nil) + } + let keyDictionary = NSDictionary(contentsOfFile: path) + + if let keyDictionary = keyDictionary, let apiKey = keyDictionary["apiKey"] as? String { + self.apiKey = apiKey + } else { + let errorMessage = + "\n------------------------------- Warning! -------------------------------\n" + + "Please, provide your own OpenWeatherMap API key in \(fileName)\n" + + "according to ConfigExample.plist\n" + + "------------------------------------------------------------------------\n" + + throw AutolabsError(code: .apiKeyNotFoundInConfigFile, + message: errorMessage, + cause: nil) + } + } +} diff --git a/AutolabsTask/AutolabsTask/ConfigExample.plist b/AutolabsTask/AutolabsTask/ConfigExample.plist new file mode 100644 index 0000000..2114469 --- /dev/null +++ b/AutolabsTask/AutolabsTask/ConfigExample.plist @@ -0,0 +1,8 @@ + + + + + apiKey + Your OpenWeatherMap API key comes here! + + diff --git a/AutolabsTask/AutolabsTask/Error.swift b/AutolabsTask/AutolabsTask/Error.swift new file mode 100644 index 0000000..b469145 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Error.swift @@ -0,0 +1,38 @@ +// +// Error.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +struct AutolabsError { + let code: ErrorCode + let message: String + let cause: Error? +} + +extension AutolabsError: CustomStringConvertible { + var description: String { return message } +} + +extension AutolabsError: CustomNSError { + var errorCode: Int { return code.rawValue } + var errorUserInfo: [String : Any] { + return [ + NSLocalizedDescriptionKey: message, + NSUnderlyingErrorKey: cause as Any + ] + } + static let errorDomain: String = "com.germanautolabs.task" +} + +enum ErrorCode: Int { + case invalidURL = 1 + case invalidJSON = 2 + case httpError = 3 + case configFileNotFound = 4 + case apiKeyNotFoundInConfigFile = 5 +} diff --git a/AutolabsTask/AutolabsTask/Info.plist b/AutolabsTask/AutolabsTask/Info.plist new file mode 100644 index 0000000..a1e3bec --- /dev/null +++ b/AutolabsTask/AutolabsTask/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + To be able to provide your local weather information + NSMicrophoneUsageDescription + Sorry, can't hear you! + NSSpeechRecognitionUsageDescription + Lost in translation + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/AutolabsTask/AutolabsTask/Location/LocationProvider.swift b/AutolabsTask/AutolabsTask/Location/LocationProvider.swift new file mode 100644 index 0000000..f984b20 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Location/LocationProvider.swift @@ -0,0 +1,15 @@ +// +// LocationProvider.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation +import CoreLocation + +protocol LocationProvider { + func refresh() + var currentLocation: CLLocationCoordinate2D? { get } +} diff --git a/AutolabsTask/AutolabsTask/Location/UserLocationProvider.swift b/AutolabsTask/AutolabsTask/Location/UserLocationProvider.swift new file mode 100644 index 0000000..78aae3b --- /dev/null +++ b/AutolabsTask/AutolabsTask/Location/UserLocationProvider.swift @@ -0,0 +1,57 @@ +// +// UserLocationProvider.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation +import CoreLocation + +class UserLocationProvider: NSObject, LocationProvider{ + + fileprivate let locationManager = CLLocationManager() + + var currentLocation: CLLocationCoordinate2D? + + override init() { + super.init() + self.locationManager.requestWhenInUseAuthorization() + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters + } + + func refresh() { + if CLLocationManager.locationServicesEnabled() { + switch CLLocationManager.authorizationStatus() { + case .notDetermined, .restricted, .denied: + print("Location not accessed") + return + case .authorizedAlways, .authorizedWhenInUse: + print("Location accessed") + } + } else { + print("Location services not enabled") + return + } + + locationManager.startUpdatingLocation() + } +} + +extension UserLocationProvider: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let coordinate: CLLocationCoordinate2D = manager.location?.coordinate else { + return + } + locationManager.stopUpdatingLocation() + self.currentLocation = coordinate + } + + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + if status == .authorizedAlways || status == .authorizedWhenInUse { + locationManager.startUpdatingLocation() + } + } +} diff --git a/AutolabsTask/AutolabsTask/Models/Weather.swift b/AutolabsTask/AutolabsTask/Models/Weather.swift new file mode 100644 index 0000000..360efb2 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Models/Weather.swift @@ -0,0 +1,13 @@ +// +// Weather.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +struct Weather: Codable { + let main: String +} diff --git a/AutolabsTask/AutolabsTask/Networking/OpenWeatherMapWebService.swift b/AutolabsTask/AutolabsTask/Networking/OpenWeatherMapWebService.swift new file mode 100644 index 0000000..ff9b1e6 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Networking/OpenWeatherMapWebService.swift @@ -0,0 +1,106 @@ +// +// WebService.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + + +enum HTTPMethod: String { + case get = "GET" +} + +extension Dictionary where Key == String, Value == String { + var queryString: String { + return map({ "\($0.0)=\($0.1.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!)" }).joined(separator: "&") + } +} + +protocol Request { + associatedtype Resp: Response + + static var path: String { get } + static var httpMethod: HTTPMethod { get } + + var query: [String: String] { get } +} + +protocol Response: Decodable {} + +struct ResponseWrapper: Decodable { + let response: Resp +} + + +class OpenWeatherMapWebService: WebService { + + private let apiBaseURL = "https://api.openweathermap.org/data/2.5" + private let apiKey: String + + private let jsonEncoder = JSONEncoder() + private let jsonDecoder = JSONDecoder() + + private let urlSession = URLSession(configuration: .default) + + + required init(apiKey: String) { + self.apiKey = apiKey + } + + func request(request: Req, completion: @escaping (Result) -> ()) { + var queryDict = request.query + + queryDict["APPID"] = apiKey + + let queryString = queryDict.queryString + let urlString = "\(apiBaseURL)/\(Req.path)?\(queryString)" + + guard let url = URL(string: urlString) else { + completion(.failure(AutolabsError(code: .invalidURL, message: "Invalid URL", cause: nil))) + return + } + + var urlRequest = URLRequest(url: url) + + urlRequest.httpMethod = Req.httpMethod.rawValue + urlRequest.setValue("application/json", forHTTPHeaderField: "content-type") + urlRequest.setValue("application/json", forHTTPHeaderField: "accept") + + let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in + if error != nil { + completion(.failure(AutolabsError(code: .httpError, message: "Failed to send request", cause: error))) + return + } + + guard let response = response as? HTTPURLResponse else { + completion(.failure(AutolabsError(code: .httpError, message: "Bad response", cause: error))) + return + } + + + if response.statusCode < 200 || 300 <= response.statusCode { + completion(.failure(AutolabsError(code: .httpError, message: "Bad response", cause: error))) + } + + guard let data = data else { + completion(.failure(AutolabsError(code: .httpError, message: "No data received", cause: nil))) + return + } + + let responseObj: Req.Resp + do { + responseObj = try self.jsonDecoder.decode(Req.Resp.self, from: data) + } catch { + completion(.failure(AutolabsError(code: .invalidURL, message: "Invalid JSON", cause: error))) + return + } + + completion(.success(responseObj)) + } + + task.resume() + } +} diff --git a/AutolabsTask/AutolabsTask/Networking/WeatherByCityName.swift b/AutolabsTask/AutolabsTask/Networking/WeatherByCityName.swift new file mode 100644 index 0000000..312972b --- /dev/null +++ b/AutolabsTask/AutolabsTask/Networking/WeatherByCityName.swift @@ -0,0 +1,32 @@ +// +// WeatherByCityName.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +struct WeatherByCityNameRequest: Codable { + let cityName: String +} + +struct WeatherByCityNameResponse: Codable { + let weather: [Weather] + let name: String +} + +extension WeatherByCityNameRequest: Request { + typealias Resp = WeatherByCityNameResponse + + static let path: String = "weather" + static let httpMethod: HTTPMethod = .get + + var query: [String : String] { + return ["q" : cityName] + } +} + +extension WeatherByCityNameResponse: Response { +} diff --git a/AutolabsTask/AutolabsTask/Networking/WeatherByCoordinate.swift b/AutolabsTask/AutolabsTask/Networking/WeatherByCoordinate.swift new file mode 100644 index 0000000..474dd14 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Networking/WeatherByCoordinate.swift @@ -0,0 +1,34 @@ +// +// WeatherByCoordinate.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +struct WeatherByCoordinateRequest: Codable { + let latitude: String + let longitude: String +} + +struct WeatherByCoordinateResponse: Codable { + let weather: [Weather] + let name: String +} + +extension WeatherByCoordinateRequest: Request { + typealias Resp = WeatherByCoordinateResponse + + static let path: String = "weather" + static let httpMethod: HTTPMethod = .get + + var query: [String : String] { + return ["lat" : latitude, + "lon" : longitude] + } +} + +extension WeatherByCoordinateResponse: Response { +} diff --git a/AutolabsTask/AutolabsTask/Networking/WebService.swift b/AutolabsTask/AutolabsTask/Networking/WebService.swift new file mode 100644 index 0000000..e3a2b7c --- /dev/null +++ b/AutolabsTask/AutolabsTask/Networking/WebService.swift @@ -0,0 +1,13 @@ +// +// WebService.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +protocol WebService { + func request(request: Req, completion: @escaping (Result) -> ()) +} diff --git a/AutolabsTask/AutolabsTask/Result.swift b/AutolabsTask/AutolabsTask/Result.swift new file mode 100644 index 0000000..8c954bb --- /dev/null +++ b/AutolabsTask/AutolabsTask/Result.swift @@ -0,0 +1,22 @@ +// +// Result.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +public enum Result { + case success(Value) + case failure(Error) + + public init(value: Value) { + self = .success(value) + } + + public init(error: Error) { + self = .failure(error) + } +} diff --git a/AutolabsTask/AutolabsTask/Speech/SpeechProcessor.swift b/AutolabsTask/AutolabsTask/Speech/SpeechProcessor.swift new file mode 100644 index 0000000..2b4b37a --- /dev/null +++ b/AutolabsTask/AutolabsTask/Speech/SpeechProcessor.swift @@ -0,0 +1,18 @@ +// +// SpeechProcessor.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation + +protocol SpeechProcessor { + var requestRecognized: ((String?) -> Void)? { get set } + var accessProblem: ((String) -> Void)? { get set } + var speechRecognitionStarted: (() -> Void)? { get set } + + func startSpeechRecognition() + func stopSpeechRecognition() +} diff --git a/AutolabsTask/AutolabsTask/Speech/UserSpeechProcessor.swift b/AutolabsTask/AutolabsTask/Speech/UserSpeechProcessor.swift new file mode 100644 index 0000000..46f8167 --- /dev/null +++ b/AutolabsTask/AutolabsTask/Speech/UserSpeechProcessor.swift @@ -0,0 +1,143 @@ +// +// UserSpeechProcessor.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import Foundation +import AVFoundation +import Speech + +class UserSpeechProcessor: NSObject, SpeechProcessor { + + private let audioEngine = AVAudioEngine() + private let speechRecognizer: SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale.init(identifier: "en-US")) + private var request: SFSpeechAudioBufferRecognitionRequest? + private var recognitionTask: SFSpeechRecognitionTask? + + var requestRecognized: ((String?) -> Void)? + var accessProblem: ((String) -> Void)? + var speechRecognitionStarted: (() -> Void)? + + let matchers: [(NSRegularExpression, (NSTextCheckingResult) -> [NSRange])] = [ + (try! NSRegularExpression(pattern: "weather like in (\\w+)"), { [$0.range(at: 1)] }), + (try! NSRegularExpression(pattern: "weather in (\\w+)"), { [$0.range(at: 1)] }), + (try! NSRegularExpression(pattern: "weather like today"), { _ in [] }), + (try! NSRegularExpression(pattern: "weather like here"), { _ in [] }), + (try! NSRegularExpression(pattern: "weather here"), { _ in [] }), + (try! NSRegularExpression(pattern: "local weather"), { _ in [] }), + (try! NSRegularExpression(pattern: "current weather"), { _ in [] }), + ] + + func startSpeechRecognition() { + requestAuthorizationsAndStartRecognition() + } + + func stopSpeechRecognition() { + if audioEngine.isRunning { + request?.endAudio() + audioEngine.inputNode.removeTap(onBus: 0) + audioEngine.inputNode.reset() + audioEngine.stop() + recognitionTask?.cancel() + recognitionTask = nil + request = nil + } + } + + func evaluateRegex(_ bestString: String) { + let nsBestString = bestString as NSString + for (regex, action) in self.matchers { + regex.enumerateMatches(in: bestString, options: [], range: NSRange(location: 0, length: nsBestString.length)) { result, _, stop in + guard let r = result else { return } + let ranges = action(r) + let texts = ranges.map { nsBestString.substring(with: $0) } + let cityName = texts.first + + stop.pointee = true + + self.stopSpeechRecognition() + self.requestRecognized?(cityName) + } + } + } + + private func requestAuthorizationsAndStartRecognition() { + SFSpeechRecognizer.requestAuthorization { authStatus in + OperationQueue.main.addOperation { + switch authStatus { + case .authorized: + switch AVAudioSession.sharedInstance().recordPermission() { + case AVAudioSessionRecordPermission.granted: + self.speechRecognitionStarted?() + self.startRecognition() + case AVAudioSessionRecordPermission.denied: + self.accessProblem?("No microphone access") + case AVAudioSessionRecordPermission.undetermined: + self.accessProblem?("No microphone access") + AVAudioSession.sharedInstance().requestRecordPermission({ granted in + if granted { + print("Microphone access granted") + } + }) + } + case .denied: + self.accessProblem?("No access to speech recognition") + case .restricted: + self.accessProblem?("Speech recognition restricted") + case .notDetermined: + self.accessProblem?("Speech recognition restricted") + } + } + } + } + + private func startRecognition() { + request = SFSpeechAudioBufferRecognitionRequest() + + guard let recognizer = SFSpeechRecognizer(locale: Locale.init(identifier: "en-US")) else { + print("Recognizer error") + return + } + + if !recognizer.isAvailable { + print("Recognizer not available") + self.accessProblem?("Speech recognizer not available") + return + } + + let node = audioEngine.inputNode + node.removeTap(onBus: 0) // To prevent accidental crashes + let recordingFormat = node.outputFormat(forBus: 0) + node.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in + self.request?.append(buffer) + } + + audioEngine.prepare() + do { + try audioEngine.start() + } catch { + print("Audio engine fail") + return print(error) + } + + if let request = request { + recognitionTask = speechRecognizer?.recognitionTask(with: request, resultHandler: { result, error in + if let result = result { + DispatchQueue.main.async() { + if !self.audioEngine.isRunning { + return + } + let bestString = result.bestTranscription.formattedString.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + + self.evaluateRegex(bestString) + } + } else if let error = error { + print(error) + } + }) + } + } +} diff --git a/AutolabsTask/AutolabsTask/ViewControllers/WeatherViewController.swift b/AutolabsTask/AutolabsTask/ViewControllers/WeatherViewController.swift new file mode 100644 index 0000000..cc72673 --- /dev/null +++ b/AutolabsTask/AutolabsTask/ViewControllers/WeatherViewController.swift @@ -0,0 +1,104 @@ +// +// WeatherViewController.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 25.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import UIKit + +class WeatherViewController: UIViewController { + + var background1ImageView = UIImageView() + var background2ImageView = UIImageView() + var background3ImageView = UIImageView() + + var userMessageLabel = UILabel() + + override func viewDidLoad() { + super.viewDidLoad() + + background1ImageView.transform = CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.height) + background2ImageView.transform = CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.height) + + userMessageLabel.alpha = 0.0 + + placeParallaxImageView(background3ImageView, motionEffectRatio: 0.02) + placeParallaxImageView(background2ImageView, motionEffectRatio: 0.045) + placeParallaxImageView(background1ImageView, motionEffectRatio: 0.08) + + placeUserMessageLabel() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + UIView.animate(withDuration: 0.9, + delay: 0.0, + options: .curveEaseOut, + animations: { self.background1ImageView.transform = CGAffineTransform.identity } + ) + + UIView.animate(withDuration: 0.9, + delay: 0.2, + options: .curveEaseOut, + animations: { self.background2ImageView.transform = CGAffineTransform.identity } + ) + + UIView.animate(withDuration: 0.5, + delay: 0.6, + animations: { self.userMessageLabel.alpha = 1 } + ) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + func placeParallaxImageView(_ imageView: UIImageView, motionEffectRatio: CGFloat) { + view.addSubview(imageView) + + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + + let smallerEdge = min(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + let horizontalMotionEffectConstant = smallerEdge * motionEffectRatio + let verticalMotionEffectConstant = smallerEdge * motionEffectRatio + + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -horizontalMotionEffectConstant).isActive = true + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: horizontalMotionEffectConstant).isActive = true + imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: -verticalMotionEffectConstant).isActive = true + imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: verticalMotionEffectConstant).isActive = true + + imageView.clipsToBounds = true + + let horizontalMotionEffect = UIInterpolatingMotionEffect(keyPath: "layer.position.x", type: .tiltAlongHorizontalAxis) + horizontalMotionEffect.minimumRelativeValue = -horizontalMotionEffectConstant + horizontalMotionEffect.maximumRelativeValue = horizontalMotionEffectConstant + imageView.addMotionEffect(horizontalMotionEffect) + + let verticalMotionEffect = UIInterpolatingMotionEffect(keyPath: "layer.position.y", type: .tiltAlongVerticalAxis) + verticalMotionEffect.minimumRelativeValue = -verticalMotionEffectConstant + verticalMotionEffect.maximumRelativeValue = verticalMotionEffectConstant + imageView.addMotionEffect(verticalMotionEffect) + } + + func placeUserMessageLabel() { + view.addSubview(userMessageLabel) + + let smallerEdge = min(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + let sizeRatio: CGFloat = 0.9 + + userMessageLabel.textColor = .white + userMessageLabel.adjustsFontSizeToFitWidth = true + userMessageLabel.textAlignment = .center + userMessageLabel.font = UIFont.boldSystemFont(ofSize: 60.0) + + userMessageLabel.translatesAutoresizingMaskIntoConstraints = false + userMessageLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + userMessageLabel.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + userMessageLabel.bottomAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + userMessageLabel.widthAnchor.constraint(equalToConstant: smallerEdge * sizeRatio).isActive = true + } +} diff --git a/AutolabsTask/AutolabsTask/WeatherUseCase.swift b/AutolabsTask/AutolabsTask/WeatherUseCase.swift new file mode 100644 index 0000000..eefd9c0 --- /dev/null +++ b/AutolabsTask/AutolabsTask/WeatherUseCase.swift @@ -0,0 +1,157 @@ +// +// WeatherUseCase.swift +// AutolabsTask +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import UIKit + +class WeatherUseCase { + + private var webService: WebService + private var speechProcessor: SpeechProcessor + var locationProvider: LocationProvider + var viewController: WeatherViewController + + public var userMessage = "" { + didSet { + viewController.userMessageLabel.text = userMessage + } + } + + private var background1Image = UIImage() { + didSet { + viewController.background1ImageView.image = background1Image + } + } + + private var background2Image = UIImage() { + didSet { + viewController.background2ImageView.image = background2Image + } + } + + private var background3Image = UIImage() { + didSet { + viewController.background3ImageView.image = background3Image + } + } + + // TODO: Expand with more options and images! + private let backgroundImageMap = [ + "sunny" : [ + "1" : "sunny1", + "2" : "sunny2", + "3" : "sunny3" + ], + "default" : [ + "1" : "sunny1", + "2" : "sunny2", + "3" : "sunny3" + ], + ] + + required init(viewController: WeatherViewController, webService: WebService, speechProcessor: SpeechProcessor, locationProvider: LocationProvider) { + self.viewController = viewController + self.webService = webService + self.speechProcessor = speechProcessor + self.locationProvider = locationProvider + + viewController.userMessageLabel.text = "Ask the current weather..." + + viewController.background1ImageView.image = UIImage(named: self.backgroundImageMap["default"]!["1"]!)! + viewController.background2ImageView.image = UIImage(named: self.backgroundImageMap["default"]!["2"]!)! + viewController.background3ImageView.image = UIImage(named: self.backgroundImageMap["default"]!["3"]!)! + + self.locationProvider.refresh() + self.speechProcessor.startSpeechRecognition() + + self.speechProcessor.requestRecognized = { (cityName) in + if cityName != nil { + self.webService.request(request: WeatherByCityNameRequest(cityName: cityName!), + completion: { (result) in + DispatchQueue.main.async() { + switch result { + case .success(let response): + print(response) + guard let weather = response.weather.first else { + break + } + self.userMessage = "\(weather.main) in \(response.name)" + self.backgroundImageMapper(weather: weather.main.lowercased()) + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { + speechProcessor.startSpeechRecognition() + } + case .failure(let error): + print(error) + self.userMessage = "Couldn't understand the location or something went wrong" + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { + speechProcessor.startSpeechRecognition() + } + } + } + }) + } else if locationProvider.currentLocation != nil { + + let latitude = String(locationProvider.currentLocation!.latitude) + let longitude = String(locationProvider.currentLocation!.longitude) + + self.webService.request(request: WeatherByCoordinateRequest(latitude: latitude, longitude: longitude), + completion: { (result) in + DispatchQueue.main.async() { + switch result { + case .success(let response): + print(response) + guard let weather = response.weather.first else { + break + } + self.userMessage = "\(weather.main) in \(response.name)" + self.backgroundImageMapper(weather: weather.main.lowercased()) + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { + speechProcessor.startSpeechRecognition() + } + case .failure(let error): + print(error) + self.userMessage = "Sorry, something went wrong" + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { + speechProcessor.startSpeechRecognition() + } + } + } + }) + } else { + self.userMessage = "Unknown current location" + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { + speechProcessor.startSpeechRecognition() + } + } + } + + self.speechProcessor.speechRecognitionStarted = { () in + self.userMessage = "Ask the current weather..." + } + + self.speechProcessor.accessProblem = { (errorString) in + self.userMessage = errorString + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { + speechProcessor.startSpeechRecognition() + } + } + } + + func backgroundImageMapper(weather: String) { + if let images = self.backgroundImageMap[weather] { + self.background1Image = UIImage(named: images["1"]!)! + self.background2Image = UIImage(named: images["2"]!)! + self.background3Image = UIImage(named: images["3"]!)! + } else { + self.background1Image = UIImage(named: self.backgroundImageMap["default"]!["1"]!)! + self.background2Image = UIImage(named: self.backgroundImageMap["default"]!["2"]!)! + self.background3Image = UIImage(named: self.backgroundImageMap["default"]!["3"]!)! + } + } +} diff --git a/AutolabsTask/AutolabsTaskTests/AutolabsTaskTests.swift b/AutolabsTask/AutolabsTaskTests/AutolabsTaskTests.swift new file mode 100644 index 0000000..f43b3f1 --- /dev/null +++ b/AutolabsTask/AutolabsTaskTests/AutolabsTaskTests.swift @@ -0,0 +1,102 @@ +// +// AutolabsTaskTests.swift +// AutolabsTaskTests +// +// Created by Rab Gábor on 2018. 07. 24.. +// Copyright © 2018. Rab Gábor. All rights reserved. +// + +import XCTest +import CoreLocation +@testable import AutolabsTask + +class MockUserSpeechProcessor: UserSpeechProcessor { + override func startSpeechRecognition() { } + + func startSpeechRecognition(_ text: String) { + self.speechRecognitionStarted?() + self.evaluateRegex(text) + } +} + +class MockUserLocationProvider: UserLocationProvider { + override func refresh() { + self.currentLocation = CLLocationCoordinate2D(latitude: 47.49801, longitude: 19.03991) + } +} + +class UseCaseSpy: WeatherUseCase { + var londonExpactation: XCTestExpectation? + var budapestExpactation: XCTestExpectation? + + override var userMessage: String { + didSet { + if userMessage.hasSuffix(" in London") { + londonExpactation?.fulfill() + } + else if userMessage.hasSuffix(" in Budapest") { + budapestExpactation?.fulfill() + } + self.viewController.userMessageLabel.text = userMessage + } + } +} + +class AutolabsTaskTests: XCTestCase { + + var mockSpeechProcessor: MockUserSpeechProcessor! + var webService: OpenWeatherMapWebService! + var mockLocationProvider: MockUserLocationProvider! + var useCase: UseCaseSpy! + var viewController: WeatherViewController! + + override func setUp() { + super.setUp() + + let config: Config + do { + config = try Config() + } catch { + print(error.localizedDescription) + preconditionFailure(error.localizedDescription) + } + + mockSpeechProcessor = MockUserSpeechProcessor() + webService = OpenWeatherMapWebService(apiKey: config.apiKey) + mockLocationProvider = MockUserLocationProvider() + viewController = WeatherViewController() + + useCase = UseCaseSpy(viewController: viewController, + webService: webService, + speechProcessor: mockSpeechProcessor, + locationProvider: mockLocationProvider) + } + + override func tearDown() { + mockSpeechProcessor = nil + webService = nil + mockLocationProvider = nil + viewController = nil + useCase = nil + + super.tearDown() + } + + func testCityBasedWeatherFetch() { + let expectation = self.expectation(description: "London weather displayed") + useCase.londonExpactation = expectation + + mockSpeechProcessor.startSpeechRecognition("What's the weather in London") + + wait(for: [expectation], timeout: 1.5) + } + + func testLocalWeatherFetch() { + let expectation = self.expectation(description: "Budapest weather displayed") + useCase.budapestExpactation = expectation + + mockSpeechProcessor.startSpeechRecognition("What's the weather here") + + wait(for: [expectation], timeout: 1.5) + } +} diff --git a/AutolabsTask/AutolabsTaskTests/Info.plist b/AutolabsTask/AutolabsTaskTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/AutolabsTask/AutolabsTaskTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/README.md b/README.md index f619c5b..fcca57a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ +# German Autolabs iOS coding challenge + +## Overview +The whole original challenge readme is incorporated as reference below. + +I created a nice application with simplicity and usability in mind. The interface uses parallax effect, so if you can, please try it on a device to experience it in full glory. + +The application listens when it displays "Ask the current weather...". + +You can ask for the local weather like "What's the weather here", or weather in a city like "What's the weather like in London". + +Have fun! + +## Usage + +**! IMPORTANT !** + +To run the application, you must create Config.plist (or any other property list) file in the project, with your OpenWeatherMap API key as value for key "apiKey". ConfigExample.plist is provided as reference. +If you want to name you property list file differently, you can do that by initiating Config() with your file name. +Not having a proper property list file leads to a juicy preconditionFailure, with description of the problem and how to solve it. Config.plist is git ignored. + +## Shortcomings, future plans +The speech engine stops after one minute, this shortcoming is NOT handled yet. + +The application supports different weather based displays, but the images for different weather situations are not included yet. + + + +# Original readme + + # iOS Coding Challenge We want to know how you write **code** - we don't care about coding challenges where you have to reimplement the HTTP protocol by using the bare basics, we want to know how you can use the existing libraries to solve the problems that we have to solve. @@ -18,3 +49,5 @@ After sending the challenge we'll wait 2 weeks to hear back from you. Feel free ## Process When you're ready, please fork this repository and start writing code in your fork. You'll get extra points for committing often in small chunks, so we'll see the process of how you created the application. +# Process +When you're ready, please fork this repository and start writing code in your fork. You'll get extra points for committing often in small chunks, so we'll see the process of how you created the application. \ No newline at end of file