diff --git a/.gitignore b/.gitignore index 261ddeda..08932a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,38 @@ -# Created by https://www.gitignore.io/api/xcode,swift -# Edit at https://www.gitignore.io/?templates=xcode,swift +# Created by https://www.gitignore.io/api/git,xcode +# Edit at https://www.gitignore.io/?templates=git,xcode + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt -### Swift ### +### Xcode ### # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore -## Build generated +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ - -## Various settings +*.moved-aside *.pbxuser !default.pbxuser *.mode1v3 @@ -20,78 +41,6 @@ DerivedData/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ - -## Other -*.moved-aside -*.xccheckout -*.xcscmblueprint - -## Obj-C/Swift specific -*.hmap -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -.build/ - -# CocoaPods -# -# 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/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build - -# fastlane -# -# 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://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -### Xcode ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) ### Xcode Patch ### *.xcodeproj/* @@ -101,4 +50,4 @@ iOSInjectionProject/ /*.gcno **/xcshareddata/WorkspaceSettings.xcsettings -# End of https://www.gitignore.io/api/xcode,swift +# End of https://www.gitignore.io/api/git,xcode diff --git a/.swiftlint.yml b/.swiftlint.yml index 006d79d9..b7d116fe 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,10 +1,18 @@ disabled_rules: - leading_whitespace - trailing_whitespace +- nesting excluded: - FineDust/Supporting Files/AppDelegate.swift +- FineDust/Supporting Files/AppDelegate.swift +- FineDust/Supporting Files/GeoConverter.swift line_length: -warning: 99 -error: 120 + warning: 99 + error: 120 + +identifier_name: + excluded: + - x + - y + - dx diff --git a/FineDust.xcodeproj/project.pbxproj b/FineDust.xcodeproj/project.pbxproj index f4afcd84..17e1b70d 100644 --- a/FineDust.xcodeproj/project.pbxproj +++ b/FineDust.xcodeproj/project.pbxproj @@ -7,152 +7,454 @@ objects = { /* Begin PBXBuildFile section */ - 192CDA8B21F5EC8D000CE35D /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA8A21F5EC8D000CE35D /* MainViewController.swift */; }; - 192CDA8D21F5ED9C000CE35D /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA8C21F5ED9C000CE35D /* Network.swift */; }; - 192CDA8F21F5EDFC000CE35D /* NSObject+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA8E21F5EDFC000CE35D /* NSObject+.swift */; }; - 192CDA9121F5EE04000CE35D /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA9021F5EE04000CE35D /* UIViewController+.swift */; }; - 192CDA9321F5EE09000CE35D /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA9221F5EE09000CE35D /* UIView+.swift */; }; - 192CDA9521F5EE11000CE35D /* UIAlertController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA9421F5EE11000CE35D /* UIAlertController+.swift */; }; - 192CDA9721F5EE17000CE35D /* UIColor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA9621F5EE17000CE35D /* UIColor+.swift */; }; - 192CDA9921F5EE1E000CE35D /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA9821F5EE1E000CE35D /* String+.swift */; }; - 192CDA9D21F5F27A000CE35D /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192CDA9C21F5F27A000CE35D /* HTTPMethod.swift */; }; - 1949B09921F5EB3200B22915 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1949B09821F5EB3200B22915 /* AppDelegate.swift */; }; - 1949B09E21F5EB3200B22915 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1949B09C21F5EB3200B22915 /* Main.storyboard */; }; - 1949B0A121F5EB3200B22915 /* FineDust.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1949B09F21F5EB3200B22915 /* FineDust.xcdatamodeld */; }; - 1949B0A321F5EB3600B22915 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1949B0A221F5EB3600B22915 /* Assets.xcassets */; }; - 1949B0A621F5EB3600B22915 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1949B0A421F5EB3600B22915 /* LaunchScreen.storyboard */; }; - 1949B0AF21F5EB9F00B22915 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1949B0AE21F5EB9F00B22915 /* .swiftlint.yml */; }; + 204D5CCDCF0215098F02993D13C3EBFE /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 90760EFD0D5A2BDBAF4D75058A09EC0A /* .swiftlint.yml */; }; + 608B6E48665600090634A3AB9AECC709 /* API+FineDust.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0CDE510AD261B4ABEBB51E5C92A814 /* API+FineDust.swift */; }; + 007C0D8821FEE03700D19796 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007C0D8721FEE03700D19796 /* HealthKitManager.swift */; }; + 007C0D8A21FEE4EC00D19796 /* Double+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007C0D8921FEE4EC00D19796 /* Double+.swift */; }; + 13240249FF2FAEEC9CF3EF4870C8C3A1 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11E1C76C986B84F5A93CD0A08B54BFB /* API.swift */; }; + DCE1D84855AA9E770D56DBD295DE412E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D208A0B65663B22067E6C7CE03E25453 /* AppDelegate.swift */; }; + 761345E0591820A820DBB44A79B1FB76 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F57EA71672D592C503198144E3648E /* Assets.swift */; }; + 740464EE62794055771F2E4C9D0035C0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4ABB05FB0A25453FC7A9EE002373931 /* Assets.xcassets */; }; + 0902E7B6288B1F6B38A1FA941EF9D737 /* CALayer+.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD13F7185E86398B09AE891F1343AA5E /* CALayer+.swift */; }; + BA45B98CB11CC8D859EBA76342CF6029 /* Common.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 837F021C44F34498596E7FCE85A20B51 /* Common.storyboard */; }; + E7863FA24D4CC1DD1C8809C51CE226CB /* CoreData+Intake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35887A35006CD23F2F658B61865C94EE /* CoreData+Intake.swift */; }; + 254F4FA6169B39876343EB37D9DFD50F /* CoreData+User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608282B25B2C8A8C81584C48DAD15FA5 /* CoreData+User.swift */; }; + A093DBC10FD94BC2458530FB4ED46E46 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5334EFD5FD1ABA67CF0ABBA8D3035AEC /* CoreDataManager.swift */; }; + 959228DE936B1B4EB610C4992BB3F45C /* DataTerm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB3A0C58F91C851019700A53DC3C53D /* DataTerm.swift */; }; + 485C2BFFB3B3A55ECAA2EA3E5E665B3F /* Date+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804690DF390A0D47D42527A7CB7C6101 /* Date+.swift */; }; + AF73755646D309F02637E4EE66CD23C5 /* Feedback.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 907A79A2E70A8E037ABE9DC8A9E81490 /* Feedback.storyboard */; }; + 204B29640C67AFB04114BBFBF8CD2FA0 /* FeedbackCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC7653C776E1A3E19A919FF1CA3E8B01 /* FeedbackCollectionViewCell.swift */; }; + C227239079D47CDEC9F72FAEDA56B178 /* FeedbackListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059EEA2F5DED858EF5951E9018C8A75 /* FeedbackListTableViewCell.swift */; }; + 20CA4CC9EEF0809549D22652D5C0C4D4 /* FeedbackListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8438C43EE32ED8F13670899C1B1F531 /* FeedbackListViewController.swift */; }; + F6C1234488BC54FDB5C5D472D24EDC2D /* FineDust.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 17AFECFD5FB32629FADDDD4FA8975772 /* FineDust.xcdatamodeld */; }; + 20BE30616C402C6AE0C7FAB843808747 /* FineDustHK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EDFAF0DF8D1A91CF045DFB46C9217C /* FineDustHK.swift */; }; + 232662E5D1D5338B147FF05959D797E1 /* FineDustInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6647CE1606E3F04A904663D01E53B70C /* FineDustInfo.swift */; }; + 9B87020C83CE101BD244F0BFF6F082BE /* FineDustResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2A9DF5E234F0ACFA0BA27369F27AB6 /* FineDustResponse.swift */; }; + 0275B02EDF3C2DFDB6A00E742F931121 /* FineDustTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0570DA836BCD4B477526BB294F8A2C18 /* FineDustTests.swift */; }; + 204861CD1D98F7238EA6235CF28AB641 /* GeoConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3AAD051DF7FFF74B2A70DEBC077F4 /* GeoConverter.swift */; }; + FD2BDBBAA3B5982381BFD1376572451D /* GeoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDC11CBF6CF397C8B884B856EB0695C /* GeoInfo.swift */; }; + 0340ABA98AAE5B4F754E2E330CD05303 /* Grade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E654EDC242A66C1EC19FE34A7991FF /* Grade.swift */; }; + 519B7F0016C2C381A823F4E7629D0A85 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E26FB8E681522D59A6D20304C6C35C8 /* HealthKit.framework */; }; + A91D83975CED5DB65ED352B523335AB2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D764E2C19ACD93F21236E2C52D091FFB /* LaunchScreen.storyboard */; }; + B25603901D040739AF76785FA51E9BB5 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2208DA1AD0364B9DD7B4800E86288437 /* LocationManager.swift */; }; + 5C7A02F9CEE3F4A4A79E457AFEDCEBAF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EE1273B4F655106F72F602D38FC779D2 /* Main.storyboard */; }; + DDF20BF1181CF217DC8739371363142A /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384B12B899B6BF360FA830E35534B006 /* MainViewController.swift */; }; + 1F8405E32EEF66BD14AAC45BD82369DD /* NSLayoutAnchor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978E9708A1C5C90982083446DDFDF57E /* NSLayoutAnchor+.swift */; }; + 41057EA496BADCA9B72504DFC9FFA23B /* NSLayoutConstraint+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475FEB0148B502284938C322A1FCE657 /* NSLayoutConstraint+.swift */; }; + C33546D723737ADFD171EBB0CD18C1BF /* NSLayoutDimension+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0BD575BEB17D9D2493F29578956C779 /* NSLayoutDimension+.swift */; }; + 2E0344EDA34A627A756BC20F9B2A791F /* NSObject+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE7F648E3F1AFEF8F2E912807BD956C /* NSObject+.swift */; }; + BD5CD8B8D765B77C2BE845AAFB2C42B0 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15B738736AAAF1EF22743A4F13EB1C98 /* Network.swift */; }; + 63E1084724B6FC24B6AC7F1C57CEDBFB /* Notification.Name+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FF7D101FBB1B823A1C8D8B4D842060 /* Notification.Name+.swift */; }; + 67B90B88B78FC798FA44EB480AB2167A /* ObservatoryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4AD95023734A3CADFEB5F089113191 /* ObservatoryResponse.swift */; }; + F3759152B8C93999F833E6356811950C /* OpenHealthDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1128B77185BC231E96C69A5D15A96842 /* OpenHealthDelegate.swift */; }; + 61456BB8371C8910DFEA1F2E03CB6C81 /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F489B34F713CED4C59D8449DF165CA2C /* ProgressIndicator.swift */; }; + 9C21C55458FC610F7CA4A5A0C74647BD /* RatioGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A5681695EF53FED3A78954492680E /* RatioGraphView.swift */; }; + 23072DBB84A18FD77FCE8A5D0BA39883 /* RatioGraphView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 24FFACFCD6E51584FC511AB3F6734B3C /* RatioGraphView.xib */; }; + B500997103DB43D02E812611C3454782 /* Statistics.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DC70CCB69DBB1D392CC698DA0DC29071 /* Statistics.storyboard */; }; + 3D136B981C1EC99B4D6488FCAC846E16 /* StatisticsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F8F9F0BB8AECA24E182EBED21006A1 /* StatisticsViewController.swift */; }; + BFA9B24EA42B5531A4E033E71F4F8EDD /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A9E8FB49A9FF5D6049F1AA935B3EAD /* String+.swift */; }; + 902AEE393C167BFDE0698F2C1236F783 /* UIAlertController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499FC240839E34C3B6FE55023AAF42C2 /* UIAlertController+.swift */; }; + A0221EFCD87D0BA93CDD31C167E4DF2E /* UIColor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A6ED48D7730E8191DF882910D907E /* UIColor+.swift */; }; + EF1E146C27E18234A79270C70FA9D14F /* UIImageView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE68CF5C8CCE8F34FBB3ABED81BA164C /* UIImageView+.swift */; }; + F408ED0799CF004B53F4180BE8F1CACD /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05FC303D1FA77029C0B18B8C19A66650 /* UIView+.swift */; }; + 78460935F8D933BB0B289AF0A2352619 /* UIView+NSLayoutAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B4BE46BFB958F5BEA81116B5B5279F /* UIView+NSLayoutAnchor.swift */; }; + 5FDA8F214C6E9BDDA2C64DBB2ED640BD /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911E3D8169E0F1D9CFF3A6A227748F01 /* UIViewController+.swift */; }; + 0A6CD6CC5EF0CB96915A1717214C71DB /* ValueGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404F34397AB2620EFA2BD9E023064BF9 /* ValueGraphView.swift */; }; + 3A89DBC74C55C2198ADB55DFFC957511 /* ValueGraphView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 78DE7E5C7B780CC106B8E11859B67759 /* ValueGraphView.xib */; }; + 0CD1740E330718AD9F6C5C529AD7B10F /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = FE84CBEDDE8ED4F39D8B0BA3C4F7A506 /* swiftgen.yml */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 1A6B3E344D43ED51722536A51D8E0970 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 54C4C346C97282321391D3CDFE95CBC6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C359351F31356424279C6FAA7866E534; + remoteInfo = FineDust; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ - 192CDA8A21F5EC8D000CE35D /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; - 192CDA8C21F5ED9C000CE35D /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; - 192CDA8E21F5EDFC000CE35D /* NSObject+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+.swift"; sourceTree = ""; }; - 192CDA9021F5EE04000CE35D /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; - 192CDA9221F5EE09000CE35D /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; }; - 192CDA9421F5EE11000CE35D /* UIAlertController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+.swift"; sourceTree = ""; }; - 192CDA9621F5EE17000CE35D /* UIColor+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+.swift"; sourceTree = ""; }; - 192CDA9821F5EE1E000CE35D /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; - 192CDA9C21F5F27A000CE35D /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; - 1949B09521F5EB3200B22915 /* FineDust.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FineDust.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 1949B09821F5EB3200B22915 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 1949B09D21F5EB3200B22915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 1949B0A021F5EB3200B22915 /* FineDust.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FineDust.xcdatamodel; sourceTree = ""; }; - 1949B0A221F5EB3600B22915 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 1949B0A521F5EB3600B22915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 1949B0A721F5EB3600B22915 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 1949B0AE21F5EB9F00B22915 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; + 90760EFD0D5A2BDBAF4D75058A09EC0A /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; + AF0CDE510AD261B4ABEBB51E5C92A814 /* API+FineDust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+FineDust.swift"; sourceTree = ""; }; + B11E1C76C986B84F5A93CD0A08B54BFB /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + D208A0B65663B22067E6C7CE03E25453 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 51F57EA71672D592C503198144E3648E /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; + F4ABB05FB0A25453FC7A9EE002373931 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 007C0D8721FEE03700D19796 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = ""; }; + 007C0D8921FEE4EC00D19796 /* Double+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+.swift"; sourceTree = ""; }; + 067E449E7BAAA91C4EE4A2715980EAFD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 9FB0BC76B2D4F22A9A11E837F8E6FA5B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AD13F7185E86398B09AE891F1343AA5E /* CALayer+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+.swift"; sourceTree = ""; }; + 837F021C44F34498596E7FCE85A20B51 /* Common.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Common.storyboard; sourceTree = ""; }; + 35887A35006CD23F2F658B61865C94EE /* CoreData+Intake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreData+Intake.swift"; sourceTree = ""; }; + 608282B25B2C8A8C81584C48DAD15FA5 /* CoreData+User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreData+User.swift"; sourceTree = ""; }; + 5334EFD5FD1ABA67CF0ABBA8D3035AEC /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + DCB3A0C58F91C851019700A53DC3C53D /* DataTerm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTerm.swift; sourceTree = ""; }; + 804690DF390A0D47D42527A7CB7C6101 /* Date+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+.swift"; sourceTree = ""; }; + 907A79A2E70A8E037ABE9DC8A9E81490 /* Feedback.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Feedback.storyboard; sourceTree = ""; }; + AC7653C776E1A3E19A919FF1CA3E8B01 /* FeedbackCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackCollectionViewCell.swift; path = FineDust/Feedback/View/FeedbackCollectionViewCell.swift; sourceTree = SOURCE_ROOT; }; + B059EEA2F5DED858EF5951E9018C8A75 /* FeedbackListTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackListTableViewCell.swift; path = FineDust/Feedback/View/FeedbackListTableViewCell.swift; sourceTree = SOURCE_ROOT; }; + E8438C43EE32ED8F13670899C1B1F531 /* FeedbackListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackListViewController.swift; sourceTree = ""; }; + 6CB7A59C01A6BEA73951091DC6C6C60F /* FineDust.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FineDust.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DC90C952E7EA68296C391AC855C57729 /* FineDust.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FineDust.entitlements; sourceTree = ""; }; + D247F6F3851E8A9D8F03470A313986A4 /* FineDust.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FineDust.xcdatamodel; sourceTree = ""; }; + 07EDFAF0DF8D1A91CF045DFB46C9217C /* FineDustHK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FineDustHK.swift; sourceTree = ""; }; + 6647CE1606E3F04A904663D01E53B70C /* FineDustInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FineDustInfo.swift; sourceTree = ""; }; + 6D2A9DF5E234F0ACFA0BA27369F27AB6 /* FineDustResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FineDustResponse.swift; sourceTree = ""; }; + 0570DA836BCD4B477526BB294F8A2C18 /* FineDustTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FineDustTests.swift; sourceTree = ""; }; + 986780C6CD66A004B310FDEEC0E52DE5 /* FineDustTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FineDustTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9DD3AAD051DF7FFF74B2A70DEBC077F4 /* GeoConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoConverter.swift; sourceTree = ""; }; + CEDC11CBF6CF397C8B884B856EB0695C /* GeoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoInfo.swift; sourceTree = ""; }; + 17E654EDC242A66C1EC19FE34A7991FF /* Grade.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Grade.swift; sourceTree = ""; }; + 1E26FB8E681522D59A6D20304C6C35C8 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; + 254F9879FFF9396ECD229DAD5BAAA8F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 82887C16FE58890774E3F9057F98EFDF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2208DA1AD0364B9DD7B4800E86288437 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; + 384B12B899B6BF360FA830E35534B006 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + 978E9708A1C5C90982083446DDFDF57E /* NSLayoutAnchor+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutAnchor+.swift"; sourceTree = ""; }; + 475FEB0148B502284938C322A1FCE657 /* NSLayoutConstraint+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+.swift"; sourceTree = ""; }; + C0BD575BEB17D9D2493F29578956C779 /* NSLayoutDimension+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutDimension+.swift"; sourceTree = ""; }; + 5DE7F648E3F1AFEF8F2E912807BD956C /* NSObject+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+.swift"; sourceTree = ""; }; + 15B738736AAAF1EF22743A4F13EB1C98 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + D4FF7D101FBB1B823A1C8D8B4D842060 /* Notification.Name+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification.Name+.swift"; sourceTree = ""; }; + AF4AD95023734A3CADFEB5F089113191 /* ObservatoryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservatoryResponse.swift; sourceTree = ""; }; + 1128B77185BC231E96C69A5D15A96842 /* OpenHealthDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHealthDelegate.swift; sourceTree = ""; }; + F489B34F713CED4C59D8449DF165CA2C /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; + 5C1A5681695EF53FED3A78954492680E /* RatioGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatioGraphView.swift; sourceTree = ""; }; + 24FFACFCD6E51584FC511AB3F6734B3C /* RatioGraphView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RatioGraphView.xib; sourceTree = ""; }; + DC70CCB69DBB1D392CC698DA0DC29071 /* Statistics.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Statistics.storyboard; sourceTree = ""; }; + 10F8F9F0BB8AECA24E182EBED21006A1 /* StatisticsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsViewController.swift; sourceTree = ""; }; + 149E1E1DBA2DF694E0AE33D493CDE8F9 /* Storyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storyboard.swift; sourceTree = ""; }; + 43A9E8FB49A9FF5D6049F1AA935B3EAD /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; + 499FC240839E34C3B6FE55023AAF42C2 /* UIAlertController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+.swift"; sourceTree = ""; }; + DB9A6ED48D7730E8191DF882910D907E /* UIColor+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+.swift"; sourceTree = ""; }; + BE68CF5C8CCE8F34FBB3ABED81BA164C /* UIImageView+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+.swift"; sourceTree = ""; }; + 05FC303D1FA77029C0B18B8C19A66650 /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; }; + 98B4BE46BFB958F5BEA81116B5B5279F /* UIView+NSLayoutAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+NSLayoutAnchor.swift"; sourceTree = ""; }; + 911E3D8169E0F1D9CFF3A6A227748F01 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; + 404F34397AB2620EFA2BD9E023064BF9 /* ValueGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueGraphView.swift; sourceTree = ""; }; + 78DE7E5C7B780CC106B8E11859B67759 /* ValueGraphView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ValueGraphView.xib; sourceTree = ""; }; + FE84CBEDDE8ED4F39D8B0BA3C4F7A506 /* swiftgen.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = swiftgen.yml; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 1949B09221F5EB3200B22915 /* Frameworks */ = { + E87754171430F44C0EC85EEBB4802850 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; + 6D2B679EF123BDF10961394D11352F24 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 519B7F0016C2C381A823F4E7629D0A85 /* HealthKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1949B08C21F5EB3200B22915 = { + 006364E06753471BD3980E1060417D05 /* Manager */ = { + isa = PBXGroup; + children = ( + AF0CDE510AD261B4ABEBB51E5C92A814 /* API+FineDust.swift */, + B11E1C76C986B84F5A93CD0A08B54BFB /* API.swift */, + 5334EFD5FD1ABA67CF0ABBA8D3035AEC /* CoreDataManager.swift */, + 2208DA1AD0364B9DD7B4800E86288437 /* LocationManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + 010570675998A4B1D218BEBD271435D3 /* Statistics */ = { + isa = PBXGroup; + children = ( + 5EF2A4B3205B6DFFF576B0628D40B3CD /* Controller */, + 2A5D5F519E02A93D262BAB837E65CA3A /* View */, + DC70CCB69DBB1D392CC698DA0DC29071 /* Statistics.storyboard */, + ); + path = Statistics; + sourceTree = ""; + }; + 03539B93491D052777A42E866616EFFC = { + isa = PBXGroup; + children = ( + 481AEAA395D9C3B4FF33E868D70E8D0B /* FineDust */, + 0E84DD7C0F0B4677567895878824A5A4 /* FineDustTests */, + 6FEB335CB38ABE1473DC1DA5339DF46B /* Frameworks */, + 249684EBDBAFA57925A702F7F2706E9E /* Products */, + 90760EFD0D5A2BDBAF4D75058A09EC0A /* .swiftlint.yml */, + FE84CBEDDE8ED4F39D8B0BA3C4F7A506 /* swiftgen.yml */, + ); + sourceTree = ""; + }; + 07A2D5AE5AA53222F7A4CD29100E6E05 /* Controller */ = { + isa = PBXGroup; + children = ( + 384B12B899B6BF360FA830E35534B006 /* MainViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 10F15CFF826B6B000B3C9C0973EA612B /* Common */ = { + isa = PBXGroup; + children = ( + 006364E06753471BD3980E1060417D05 /* Manager */, + 837F021C44F34498596E7FCE85A20B51 /* Common.storyboard */, + F489B34F713CED4C59D8449DF165CA2C /* ProgressIndicator.swift */, + ); + path = Common; + sourceTree = ""; + }; + + 0E84DD7C0F0B4677567895878824A5A4 /* FineDustTests */ = { isa = PBXGroup; children = ( - 1949B0AE21F5EB9F00B22915 /* .swiftlint.yml */, - 1949B09721F5EB3200B22915 /* FineDust */, - 1949B09621F5EB3200B22915 /* Products */, + 0570DA836BCD4B477526BB294F8A2C18 /* FineDustTests.swift */, + 254F9879FFF9396ECD229DAD5BAAA8F3 /* Info.plist */, ); + path = FineDustTests; sourceTree = ""; }; - 1949B09621F5EB3200B22915 /* Products */ = { + + 249684EBDBAFA57925A702F7F2706E9E /* Products */ = { isa = PBXGroup; children = ( - 1949B09521F5EB3200B22915 /* FineDust.app */, + 6CB7A59C01A6BEA73951091DC6C6C60F /* FineDust.app */, + 986780C6CD66A004B310FDEEC0E52DE5 /* FineDustTests.xctest */, ); name = Products; sourceTree = ""; }; - 1949B09721F5EB3200B22915 /* FineDust */ = { + 2A5D5F519E02A93D262BAB837E65CA3A /* View */ = { isa = PBXGroup; children = ( - 1949B0A721F5EB3600B22915 /* Info.plist */, - 1949B0B421F5EBC900B22915 /* Core Data */, - 1949B0B021F5EBB000B22915 /* Network */, - 1949B0B221F5EBB900B22915 /* Common */, - 1949B0B121F5EBB500B22915 /* Model */, - 1949B09C21F5EB3200B22915 /* Main.storyboard */, - 192CDA8A21F5EC8D000CE35D /* MainViewController.swift */, - 1949B0B521F5EBD900B22915 /* Supporting Files */, - 1949B0B321F5EBC200B22915 /* Extension */, + 5D0F49B2B1B9455CA1580B57C0DE56C0 /* Ratio Graph */, + 333205C85D3BC78F21F3FA6B9932825F /* Value Graph */, + ); + path = View; + sourceTree = ""; + }; + 2F83400986839F304CBBA92DC52E3AD2 /* Core Data */ = { + isa = PBXGroup; + children = ( + 35887A35006CD23F2F658B61865C94EE /* CoreData+Intake.swift */, + 608282B25B2C8A8C81584C48DAD15FA5 /* CoreData+User.swift */, + 17AFECFD5FB32629FADDDD4FA8975772 /* FineDust.xcdatamodeld */, + ); + path = "Core Data"; + sourceTree = ""; + }; + 333205C85D3BC78F21F3FA6B9932825F /* Value Graph */ = { + isa = PBXGroup; + children = ( + 404F34397AB2620EFA2BD9E023064BF9 /* ValueGraphView.swift */, + 78DE7E5C7B780CC106B8E11859B67759 /* ValueGraphView.xib */, + ); + path = "Value Graph"; + sourceTree = ""; + }; + 481AEAA395D9C3B4FF33E868D70E8D0B /* FineDust */ = { + isa = PBXGroup; + children = ( + 10F15CFF826B6B000B3C9C0973EA612B /* Common */, + 2F83400986839F304CBBA92DC52E3AD2 /* Core Data */, + B3C2FE06B8909518407804448A15547A /* Extension */, + 98FC53B1C8D628A70A4BDB0A2693A42F /* Feedback */, + 5D59E8FBA59418EFF9F76EB07E738D2F /* HealthKit */, + 6BD00A070861AD554C16D14235787222 /* Main */, + 8C212B1139E96B3BA74BF5B54AB9EF23 /* Model */, + B2ACC9D9D6B0CB2A2D45D027C9177A26 /* Network */, + 99C815E84F2B6462022153ED674E25F2 /* Response */, + 010570675998A4B1D218BEBD271435D3 /* Statistics */, + C5254E221717B2B3CDB72F67E9F3234C /* Supporting Files */, + A9020D623D8916FA746D8E891134F78E /* SwiftGen */, + DC90C952E7EA68296C391AC855C57729 /* FineDust.entitlements */, + 82887C16FE58890774E3F9057F98EFDF /* Info.plist */, ); path = FineDust; sourceTree = ""; }; - 1949B0B021F5EBB000B22915 /* Network */ = { + 5D0F49B2B1B9455CA1580B57C0DE56C0 /* Ratio Graph */ = { isa = PBXGroup; children = ( - 192CDA9C21F5F27A000CE35D /* HTTPMethod.swift */, - 192CDA8C21F5ED9C000CE35D /* Network.swift */, + 5C1A5681695EF53FED3A78954492680E /* RatioGraphView.swift */, + 24FFACFCD6E51584FC511AB3F6734B3C /* RatioGraphView.xib */, ); - path = Network; + path = "Ratio Graph"; + sourceTree = ""; + }; + 5D59E8FBA59418EFF9F76EB07E738D2F /* HealthKit */ = { + isa = PBXGroup; + children = ( + 07EDFAF0DF8D1A91CF045DFB46C9217C /* FineDustHK.swift */, + 1128B77185BC231E96C69A5D15A96842 /* OpenHealthDelegate.swift */, + 007C0D8721FEE03700D19796 /* HealthKitManager.swift */, + ); + path = HealthKit; sourceTree = ""; }; - 1949B0B121F5EBB500B22915 /* Model */ = { + 5EF2A4B3205B6DFFF576B0628D40B3CD /* Controller */ = { isa = PBXGroup; children = ( + 10F8F9F0BB8AECA24E182EBED21006A1 /* StatisticsViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 6AC19CD7DDEBD8345E7C6A3745E98017 /* Controller */ = { + isa = PBXGroup; + children = ( + E8438C43EE32ED8F13670899C1B1F531 /* FeedbackListViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 6BD00A070861AD554C16D14235787222 /* Main */ = { + isa = PBXGroup; + children = ( + 07A2D5AE5AA53222F7A4CD29100E6E05 /* Controller */, + 6C27DD5B3B700C34D7307711A4F1A1FD /* View */, + EE1273B4F655106F72F602D38FC779D2 /* Main.storyboard */, + ); + path = Main; + sourceTree = ""; + }; + 6C27DD5B3B700C34D7307711A4F1A1FD /* View */ = { + isa = PBXGroup; + children = ( + ); + path = View; + sourceTree = ""; + }; + 6FEB335CB38ABE1473DC1DA5339DF46B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1E26FB8E681522D59A6D20304C6C35C8 /* HealthKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8C212B1139E96B3BA74BF5B54AB9EF23 /* Model */ = { + isa = PBXGroup; + children = ( + 6647CE1606E3F04A904663D01E53B70C /* FineDustInfo.swift */, + CEDC11CBF6CF397C8B884B856EB0695C /* GeoInfo.swift */, ); path = Model; sourceTree = ""; }; - 1949B0B221F5EBB900B22915 /* Common */ = { + 98FC53B1C8D628A70A4BDB0A2693A42F /* Feedback */ = { isa = PBXGroup; children = ( + 6AC19CD7DDEBD8345E7C6A3745E98017 /* Controller */, + C683749A4618A81E1E0F1CF2A8180BCB /* View */, + 907A79A2E70A8E037ABE9DC8A9E81490 /* Feedback.storyboard */, ); - path = Common; + path = Feedback; sourceTree = ""; }; - 1949B0B321F5EBC200B22915 /* Extension */ = { + 99C815E84F2B6462022153ED674E25F2 /* Response */ = { isa = PBXGroup; children = ( - 192CDA8E21F5EDFC000CE35D /* NSObject+.swift */, - 192CDA9021F5EE04000CE35D /* UIViewController+.swift */, - 192CDA9221F5EE09000CE35D /* UIView+.swift */, - 192CDA9421F5EE11000CE35D /* UIAlertController+.swift */, - 192CDA9621F5EE17000CE35D /* UIColor+.swift */, - 192CDA9821F5EE1E000CE35D /* String+.swift */, + DCB3A0C58F91C851019700A53DC3C53D /* DataTerm.swift */, + 6D2A9DF5E234F0ACFA0BA27369F27AB6 /* FineDustResponse.swift */, + 17E654EDC242A66C1EC19FE34A7991FF /* Grade.swift */, + AF4AD95023734A3CADFEB5F089113191 /* ObservatoryResponse.swift */, ); - path = Extension; + path = Response; sourceTree = ""; }; - 1949B0B421F5EBC900B22915 /* Core Data */ = { + A9020D623D8916FA746D8E891134F78E /* SwiftGen */ = { isa = PBXGroup; children = ( - 1949B09F21F5EB3200B22915 /* FineDust.xcdatamodeld */, + 51F57EA71672D592C503198144E3648E /* Assets.swift */, + 149E1E1DBA2DF694E0AE33D493CDE8F9 /* Storyboard.swift */, ); - path = "Core Data"; + path = SwiftGen; sourceTree = ""; }; - 1949B0B521F5EBD900B22915 /* Supporting Files */ = { + B2ACC9D9D6B0CB2A2D45D027C9177A26 /* Network */ = { isa = PBXGroup; children = ( - 1949B0A421F5EB3600B22915 /* LaunchScreen.storyboard */, - 1949B09821F5EB3200B22915 /* AppDelegate.swift */, - 1949B0A221F5EB3600B22915 /* Assets.xcassets */, + 15B738736AAAF1EF22743A4F13EB1C98 /* Network.swift */, + ); + path = Network; + sourceTree = ""; + }; + B3C2FE06B8909518407804448A15547A /* Extension */ = { + isa = PBXGroup; + children = ( + AD13F7185E86398B09AE891F1343AA5E /* CALayer+.swift */, + 804690DF390A0D47D42527A7CB7C6101 /* Date+.swift */, + 978E9708A1C5C90982083446DDFDF57E /* NSLayoutAnchor+.swift */, + 475FEB0148B502284938C322A1FCE657 /* NSLayoutConstraint+.swift */, + C0BD575BEB17D9D2493F29578956C779 /* NSLayoutDimension+.swift */, + 5DE7F648E3F1AFEF8F2E912807BD956C /* NSObject+.swift */, + D4FF7D101FBB1B823A1C8D8B4D842060 /* Notification.Name+.swift */, + 43A9E8FB49A9FF5D6049F1AA935B3EAD /* String+.swift */, + 499FC240839E34C3B6FE55023AAF42C2 /* UIAlertController+.swift */, + DB9A6ED48D7730E8191DF882910D907E /* UIColor+.swift */, + BE68CF5C8CCE8F34FBB3ABED81BA164C /* UIImageView+.swift */, + 05FC303D1FA77029C0B18B8C19A66650 /* UIView+.swift */, + 98B4BE46BFB958F5BEA81116B5B5279F /* UIView+NSLayoutAnchor.swift */, + 911E3D8169E0F1D9CFF3A6A227748F01 /* UIViewController+.swift */, + 007C0D8921FEE4EC00D19796 /* Double+.swift */, + ); + path = Extension; + sourceTree = ""; + }; + C5254E221717B2B3CDB72F67E9F3234C /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D208A0B65663B22067E6C7CE03E25453 /* AppDelegate.swift */, + F4ABB05FB0A25453FC7A9EE002373931 /* Assets.xcassets */, + 9DD3AAD051DF7FFF74B2A70DEBC077F4 /* GeoConverter.swift */, + D764E2C19ACD93F21236E2C52D091FFB /* LaunchScreen.storyboard */, ); path = "Supporting Files"; sourceTree = ""; }; + C683749A4618A81E1E0F1CF2A8180BCB /* View */ = { + isa = PBXGroup; + children = ( + AC7653C776E1A3E19A919FF1CA3E8B01 /* FeedbackCollectionViewCell.swift */, + B059EEA2F5DED858EF5951E9018C8A75 /* FeedbackListTableViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 1949B09421F5EB3200B22915 /* FineDust */ = { + FC670917DDB46DEF5B008F67A5D341D0 /* FineDustTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 1949B0AA21F5EB3600B22915 /* Build configuration list for PBXNativeTarget "FineDust" */; + buildConfigurationList = 817692B8CF28B7C46B6F2323E4248D6C /* Build configuration list for PBXNativeTarget "FineDustTests" */; buildPhases = ( - 1949B09121F5EB3200B22915 /* Sources */, - 1949B09221F5EB3200B22915 /* Frameworks */, - 1949B09321F5EB3200B22915 /* Resources */, - 1949B0AD21F5EB8700B22915 /* ShellScript */, + B2A27D1E2A5A7D7E39E74D3B5D2D6AF8 /* Sources */, + E87754171430F44C0EC85EEBB4802850 /* Frameworks */, + B0FB61B8DDA5BA2B81EA081952928FD8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C82ACCCC55F7C1E4DC2CE9E5CCD729AC /* PBXTargetDependency */, + ); + name = FineDustTests; + productName = FineDustTests; + productReference = 986780C6CD66A004B310FDEEC0E52DE5 /* FineDustTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + C359351F31356424279C6FAA7866E534 /* FineDust */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8F00085DE92C2EA2B2186C1D4894EE65 /* Build configuration list for PBXNativeTarget "FineDust" */; + buildPhases = ( + 825464EA5E4F501155DA8B8D147034BC /* Sources */, + 6D2B679EF123BDF10961394D11352F24 /* Frameworks */, + 51B3C28C7598053C0285EFFA1543C4F4 /* Resources */, + 7228FB920B244F6FEED29E43DBB069BD /* ShellScript */, ); buildRules = ( ); @@ -160,25 +462,37 @@ ); name = FineDust; productName = FineDust; - productReference = 1949B09521F5EB3200B22915 /* FineDust.app */; + productReference = 6CB7A59C01A6BEA73951091DC6C6C60F /* FineDust.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 1949B08D21F5EB3200B22915 /* Project object */ = { + 54C4C346C97282321391D3CDFE95CBC6 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1010; LastUpgradeCheck = 1010; ORGANIZATIONNAME = boostcamp3rd; TargetAttributes = { - 1949B09421F5EB3200B22915 = { + FC670917DDB46DEF5B008F67A5D341D0 = { + CreatedOnToolsVersion = 10.1; + TestTargetID = C359351F31356424279C6FAA7866E534; + }; + C359351F31356424279C6FAA7866E534 = { CreatedOnToolsVersion = 10.1; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 0; + }; + com.apple.HealthKit = { + enabled = 1; + }; + }; }; }; }; - buildConfigurationList = 1949B09021F5EB3200B22915 /* Build configuration list for PBXProject "FineDust" */; + buildConfigurationList = 76AB648CF61AD2E93BA56CCC29DD2E21 /* Build configuration list for PBXProject "FineDust" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; @@ -186,32 +500,46 @@ en, Base, ); - mainGroup = 1949B08C21F5EB3200B22915; - productRefGroup = 1949B09621F5EB3200B22915 /* Products */; + mainGroup = 03539B93491D052777A42E866616EFFC; + productRefGroup = 249684EBDBAFA57925A702F7F2706E9E /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 1949B09421F5EB3200B22915 /* FineDust */, + C359351F31356424279C6FAA7866E534 /* FineDust */, + FC670917DDB46DEF5B008F67A5D341D0 /* FineDustTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 1949B09321F5EB3200B22915 /* Resources */ = { + B0FB61B8DDA5BA2B81EA081952928FD8 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1949B0AF21F5EB9F00B22915 /* .swiftlint.yml in Resources */, - 1949B0A621F5EB3600B22915 /* LaunchScreen.storyboard in Resources */, - 1949B0A321F5EB3600B22915 /* Assets.xcassets in Resources */, - 1949B09E21F5EB3200B22915 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51B3C28C7598053C0285EFFA1543C4F4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 204D5CCDCF0215098F02993D13C3EBFE /* .swiftlint.yml in Resources */, + 740464EE62794055771F2E4C9D0035C0 /* Assets.xcassets in Resources */, + BA45B98CB11CC8D859EBA76342CF6029 /* Common.storyboard in Resources */, + AF73755646D309F02637E4EE66CD23C5 /* Feedback.storyboard in Resources */, + A91D83975CED5DB65ED352B523335AB2 /* LaunchScreen.storyboard in Resources */, + 5C7A02F9CEE3F4A4A79E457AFEDCEBAF /* Main.storyboard in Resources */, + 23072DBB84A18FD77FCE8A5D0BA39883 /* RatioGraphView.xib in Resources */, + B500997103DB43D02E812611C3454782 /* Statistics.storyboard in Resources */, + 3A89DBC74C55C2198ADB55DFFC957511 /* ValueGraphView.xib in Resources */, + 0CD1740E330718AD9F6C5C529AD7B10F /* swiftgen.yml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1949B0AD21F5EB8700B22915 /* ShellScript */ = { + 7228FB920B244F6FEED29E43DBB069BD /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -231,47 +559,95 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 1949B09121F5EB3200B22915 /* Sources */ = { + B2A27D1E2A5A7D7E39E74D3B5D2D6AF8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 192CDA9521F5EE11000CE35D /* UIAlertController+.swift in Sources */, - 192CDA8F21F5EDFC000CE35D /* NSObject+.swift in Sources */, - 192CDA9921F5EE1E000CE35D /* String+.swift in Sources */, - 192CDA9D21F5F27A000CE35D /* HTTPMethod.swift in Sources */, - 192CDA8B21F5EC8D000CE35D /* MainViewController.swift in Sources */, - 192CDA9321F5EE09000CE35D /* UIView+.swift in Sources */, - 192CDA9121F5EE04000CE35D /* UIViewController+.swift in Sources */, - 1949B09921F5EB3200B22915 /* AppDelegate.swift in Sources */, - 192CDA8D21F5ED9C000CE35D /* Network.swift in Sources */, - 1949B0A121F5EB3200B22915 /* FineDust.xcdatamodeld in Sources */, - 192CDA9721F5EE17000CE35D /* UIColor+.swift in Sources */, + 0275B02EDF3C2DFDB6A00E742F931121 /* FineDustTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 825464EA5E4F501155DA8B8D147034BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 608B6E48665600090634A3AB9AECC709 /* API+FineDust.swift in Sources */, + 13240249FF2FAEEC9CF3EF4870C8C3A1 /* API.swift in Sources */, + DCE1D84855AA9E770D56DBD295DE412E /* AppDelegate.swift in Sources */, + 761345E0591820A820DBB44A79B1FB76 /* Assets.swift in Sources */, + 0902E7B6288B1F6B38A1FA941EF9D737 /* CALayer+.swift in Sources */, + E7863FA24D4CC1DD1C8809C51CE226CB /* CoreData+Intake.swift in Sources */, + 254F4FA6169B39876343EB37D9DFD50F /* CoreData+User.swift in Sources */, + A093DBC10FD94BC2458530FB4ED46E46 /* CoreDataManager.swift in Sources */, + 959228DE936B1B4EB610C4992BB3F45C /* DataTerm.swift in Sources */, + 007C0D8A21FEE4EC00D19796 /* Double+.swift in Sources */, + 485C2BFFB3B3A55ECAA2EA3E5E665B3F /* Date+.swift in Sources */, + 204B29640C67AFB04114BBFBF8CD2FA0 /* FeedbackCollectionViewCell.swift in Sources */, + C227239079D47CDEC9F72FAEDA56B178 /* FeedbackListTableViewCell.swift in Sources */, + 20CA4CC9EEF0809549D22652D5C0C4D4 /* FeedbackListViewController.swift in Sources */, + F6C1234488BC54FDB5C5D472D24EDC2D /* FineDust.xcdatamodeld in Sources */, + 20BE30616C402C6AE0C7FAB843808747 /* FineDustHK.swift in Sources */, + 232662E5D1D5338B147FF05959D797E1 /* FineDustInfo.swift in Sources */, + 9B87020C83CE101BD244F0BFF6F082BE /* FineDustResponse.swift in Sources */, + 204861CD1D98F7238EA6235CF28AB641 /* GeoConverter.swift in Sources */, + FD2BDBBAA3B5982381BFD1376572451D /* GeoInfo.swift in Sources */, + 0340ABA98AAE5B4F754E2E330CD05303 /* Grade.swift in Sources */, + B25603901D040739AF76785FA51E9BB5 /* LocationManager.swift in Sources */, + DDF20BF1181CF217DC8739371363142A /* MainViewController.swift in Sources */, + 1F8405E32EEF66BD14AAC45BD82369DD /* NSLayoutAnchor+.swift in Sources */, + 41057EA496BADCA9B72504DFC9FFA23B /* NSLayoutConstraint+.swift in Sources */, + C33546D723737ADFD171EBB0CD18C1BF /* NSLayoutDimension+.swift in Sources */, + 2E0344EDA34A627A756BC20F9B2A791F /* NSObject+.swift in Sources */, + 007C0D8821FEE03700D19796 /* HealthKitManager.swift in Sources */, + BD5CD8B8D765B77C2BE845AAFB2C42B0 /* Network.swift in Sources */, + 63E1084724B6FC24B6AC7F1C57CEDBFB /* Notification.Name+.swift in Sources */, + 67B90B88B78FC798FA44EB480AB2167A /* ObservatoryResponse.swift in Sources */, + F3759152B8C93999F833E6356811950C /* OpenHealthDelegate.swift in Sources */, + 61456BB8371C8910DFEA1F2E03CB6C81 /* ProgressIndicator.swift in Sources */, + 9C21C55458FC610F7CA4A5A0C74647BD /* RatioGraphView.swift in Sources */, + 3D136B981C1EC99B4D6488FCAC846E16 /* StatisticsViewController.swift in Sources */, + BFA9B24EA42B5531A4E033E71F4F8EDD /* String+.swift in Sources */, + 902AEE393C167BFDE0698F2C1236F783 /* UIAlertController+.swift in Sources */, + A0221EFCD87D0BA93CDD31C167E4DF2E /* UIColor+.swift in Sources */, + EF1E146C27E18234A79270C70FA9D14F /* UIImageView+.swift in Sources */, + F408ED0799CF004B53F4180BE8F1CACD /* UIView+.swift in Sources */, + 78460935F8D933BB0B289AF0A2352619 /* UIView+NSLayoutAnchor.swift in Sources */, + 5FDA8F214C6E9BDDA2C64DBB2ED640BD /* UIViewController+.swift in Sources */, + 0A6CD6CC5EF0CB96915A1717214C71DB /* ValueGraphView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + C82ACCCC55F7C1E4DC2CE9E5CCD729AC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C359351F31356424279C6FAA7866E534 /* FineDust */; + targetProxy = 1A6B3E344D43ED51722536A51D8E0970 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ - 1949B09C21F5EB3200B22915 /* Main.storyboard */ = { + D764E2C19ACD93F21236E2C52D091FFB /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - 1949B09D21F5EB3200B22915 /* Base */, + 067E449E7BAAA91C4EE4A2715980EAFD /* Base */, ); - name = Main.storyboard; + name = LaunchScreen.storyboard; sourceTree = ""; }; - 1949B0A421F5EB3600B22915 /* LaunchScreen.storyboard */ = { + EE1273B4F655106F72F602D38FC779D2 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - 1949B0A521F5EB3600B22915 /* Base */, + 9FB0BC76B2D4F22A9A11E837F8E6FA5B /* Base */, ); - name = LaunchScreen.storyboard; + name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 1949B0A821F5EB3600B22915 /* Debug */ = { + 0D55646CCC95D68B755CE5C9C9012C1F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -322,7 +698,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -332,7 +708,49 @@ }; name = Debug; }; - 1949B0A921F5EB3600B22915 /* Release */ = { + 31A568A58C1334B2A5028262683C9B8D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = H79MF628K3; + INFOPLIST_FILE = FineDustTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = kr.co.boostcamp3rd.FineDustTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FineDust.app/FineDust"; + }; + name = Debug; + }; + BC40E531EFF08EFBBA68D28952F6DCFA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = H79MF628K3; + INFOPLIST_FILE = FineDustTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = kr.co.boostcamp3rd.FineDustTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FineDust.app/FineDust"; + }; + name = Release; + }; + 7F6FE804E817FE0B139A95336C65A9B3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -377,7 +795,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -387,61 +805,76 @@ }; name = Release; }; - 1949B0AB21F5EB3600B22915 /* Debug */ = { + DE8F4696B51F4C9747D985CC9765375B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = FineDust/FineDust.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = H79MF628K3; INFOPLIST_FILE = FineDust/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = co.kr.boostcamp3rd.FineDust; + PRODUCT_BUNDLE_IDENTIFIER = kr.co.boostcamp3rd.FineDust; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 1; }; - name = Debug; + name = Release; }; - 1949B0AC21F5EB3600B22915 /* Release */ = { + E56408CD67CACF8A0E4588BFF2593817 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = FineDust/FineDust.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = H79MF628K3; INFOPLIST_FILE = FineDust/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = co.kr.boostcamp3rd.FineDust; + PRODUCT_BUNDLE_IDENTIFIER = kr.co.boostcamp3rd.FineDust; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 1; }; - name = Release; + name = Debug; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 1949B09021F5EB3200B22915 /* Build configuration list for PBXProject "FineDust" */ = { + 817692B8CF28B7C46B6F2323E4248D6C /* Build configuration list for PBXNativeTarget "FineDustTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 31A568A58C1334B2A5028262683C9B8D /* Debug */, + BC40E531EFF08EFBBA68D28952F6DCFA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 76AB648CF61AD2E93BA56CCC29DD2E21 /* Build configuration list for PBXProject "FineDust" */ = { isa = XCConfigurationList; buildConfigurations = ( - 1949B0A821F5EB3600B22915 /* Debug */, - 1949B0A921F5EB3600B22915 /* Release */, + 0D55646CCC95D68B755CE5C9C9012C1F /* Debug */, + 7F6FE804E817FE0B139A95336C65A9B3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 1949B0AA21F5EB3600B22915 /* Build configuration list for PBXNativeTarget "FineDust" */ = { + 8F00085DE92C2EA2B2186C1D4894EE65 /* Build configuration list for PBXNativeTarget "FineDust" */ = { isa = XCConfigurationList; buildConfigurations = ( - 1949B0AB21F5EB3600B22915 /* Debug */, - 1949B0AC21F5EB3600B22915 /* Release */, + E56408CD67CACF8A0E4588BFF2593817 /* Debug */, + DE8F4696B51F4C9747D985CC9765375B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -449,17 +882,17 @@ /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ - 1949B09F21F5EB3200B22915 /* FineDust.xcdatamodeld */ = { + 17AFECFD5FB32629FADDDD4FA8975772 /* FineDust.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - 1949B0A021F5EB3200B22915 /* FineDust.xcdatamodel */, + D247F6F3851E8A9D8F03470A313986A4 /* FineDust.xcdatamodel */, ); - currentVersion = 1949B0A021F5EB3200B22915 /* FineDust.xcdatamodel */; + currentVersion = D247F6F3851E8A9D8F03470A313986A4 /* FineDust.xcdatamodel */; path = FineDust.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ }; - rootObject = 1949B08D21F5EB3200B22915 /* Project object */; + rootObject = 54C4C346C97282321391D3CDFE95CBC6 /* Project object */; } diff --git a/FineDust/Base.lproj/Main.storyboard b/FineDust/Base.lproj/Main.storyboard deleted file mode 100644 index b1be5cff..00000000 --- a/FineDust/Base.lproj/Main.storyboard +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/FineDust/Common/Common.storyboard b/FineDust/Common/Common.storyboard new file mode 100644 index 00000000..c047439e --- /dev/null +++ b/FineDust/Common/Common.storyboard @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FineDust/Common/Manager/API+FineDust.swift b/FineDust/Common/Manager/API+FineDust.swift new file mode 100644 index 00000000..cb05639d --- /dev/null +++ b/FineDust/Common/Manager/API+FineDust.swift @@ -0,0 +1,83 @@ +// +// API+FineDust.swift +// FineDust +// +// Created by Presto on 21/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// 미세먼지 API 관련 API 정의. +extension API { + /// 측정소 정보 조회. + /// + /// - Parameters: + /// - pageNo: 페이지 인덱스. + /// - numOfRows: 한 페이지에 노출되는 정보량. + /// - completion: 컴플리션 핸들러. + func fetchObservatory( + pageNumber pageNo: Int = 1, + numberOfRows numOfRows: Int = 10, + completion: @escaping (ObservatoryResponse?, Error?) -> Void + ) { + let urlString = baseURL + .appending("/MsrstnInfoInqireSvc/getNearbyMsrstnList") + .appending("?tmX=\(GeoInfo.shared.x)") + .appending("&tmY=\(GeoInfo.shared.y)") + .appending("&numOfRows=\(numOfRows)") + .appending("&pageNo=\(pageNo)") + .appending("&serviceKey=\(serviceKey)") + .appending("&_returnType=json") + guard let url = URL(string: urlString) else { return } + Network.request(url, method: .get) { data, error in + guard let data = data else { + completion(nil, error) + return + } + do { + let response = try JSONDecoder().decode(ObservatoryResponse.self, from: data) + completion(response, nil) + } catch { + completion(nil, error) + } + } + } + /// 미세먼지 농도 조회. + /// + /// - Parameters: + /// - dataTerm: 데이터 기간. daily 또는 month. + /// - pageNo: 페이지 인덱스. + /// - numOfRows: 한 페이지에 노출되는 정보량. + /// - completion: 컴플리션 핸들러. + func fetchFineDustConcentration( + term dataTerm: DataTerm, + pageNumber pageNo: Int = 1, + numberOfRows numOfRows: Int = 10, + completion: @escaping (FineDustResponse?, Error?) -> Void + ) { + let observatory = FineDustInfo.shared.observatory.percentEncoded + let urlString = baseURL + .appending("/ArpltnInforInqireSvc/getMsrstnAcctoRltmMesureDnsty") + .appending("?stationName=\(observatory)") + .appending("&dataTerm=\(dataTerm.rawValue)") + .appending("&pageNo=\(pageNo)") + .appending("&numOfRows=\(numOfRows)") + .appending("&serviceKey=\(serviceKey)") + .appending("&ver=1.1") + .appending("&_returnType=json") + guard let url = URL(string: urlString) else { return } + Network.request(url, method: .get) { data, error in + guard let data = data else { + completion(nil, error) + return + } + do { + let response = try JSONDecoder().decode(FineDustResponse.self, from: data) + completion(response, nil) + } catch { + completion(nil, error) + } + } + } +} diff --git a/FineDust/Common/Manager/API.swift b/FineDust/Common/Manager/API.swift new file mode 100644 index 00000000..c7acc344 --- /dev/null +++ b/FineDust/Common/Manager/API.swift @@ -0,0 +1,31 @@ +// +// API.swift +// FineDust +// +// Created by Presto on 21/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// API 정의. +final class API { + + // MARK: Singleton Object + + /// API의 싱글톤 객체 + static let shared = API() + + // MARK: Private Initializer + + private init() { } + + // MARK: Property + + /// Base URL. + let baseURL = "http://openapi.airkorea.or.kr/openapi/services/rest" + /// Service Key. + let serviceKey = """ + BfJjA4%2BuaBHhfAzyF2Ni6xoVDaf%2FhsZylifmFKdW3kyaZECH6c2Lua05fV%2F%2BYgbzPBaSl0YLZwI%2BW%2FK2xzO7sw%3D%3D + """ +} diff --git a/FineDust/Common/Manager/CoreDataManager.swift b/FineDust/Common/Manager/CoreDataManager.swift new file mode 100644 index 00000000..bbc51b5e --- /dev/null +++ b/FineDust/Common/Manager/CoreDataManager.swift @@ -0,0 +1,61 @@ +// +// CoreDataHelper.swift +// FineDust +// +// Created by Presto on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import CoreData +import Foundation +import UIKit + +/// Core Data Manager +final class CoreDataManager { + + // MARK: Singleton Object + + /// CoreDataManager의 싱글톤 객체. + static let shared = CoreDataManager() + + // MARK: Private Initializer + + private init() { } + + // MARK: Property + + /// AppDelegate에 있는 viewContext 가져오기. + private lazy var context: NSManagedObjectContext = { + guard let delegate = UIApplication.shared.delegate as? AppDelegate else { fatalError() } + return delegate.persistentContainer.viewContext + }() + + // MARK: Method + + /// 특정 타입의 데이터를 가져오기. + func fetch(forType type: T.Type, _ completion: (T?, Error?) -> Void) { + let request = NSFetchRequest(entityName: T.classNameToString) + do { + let results = try context.fetch(request) as? [T] + completion(results?.first, nil) + } catch { + completion(nil, error) + } + } + /// 특정 타입에 전달된 데이터를 저장하기. + func save( + _ dictionary: [String: Any], + forType type: T.Type, + _ completion: (Error?) -> Void + ) { + guard let entity = NSEntityDescription.entity(forEntityName: T.classNameToString, in: context) + else { return } + let newInstance = NSManagedObject(entity: entity, insertInto: context) + dictionary.forEach { newInstance.setValue($0.value, forKey: $0.key) } + do { + try context.save() + } catch { + completion(error) + } + } +} diff --git a/FineDust/Common/Manager/LocationManager.swift b/FineDust/Common/Manager/LocationManager.swift new file mode 100644 index 00000000..8bba5384 --- /dev/null +++ b/FineDust/Common/Manager/LocationManager.swift @@ -0,0 +1,14 @@ +// +// LocationManager.swift +// FineDust +// +// Created by Presto on 28/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import CoreLocation +import Foundation + +final class LocationManager { + +} diff --git a/FineDust/Common/ProgressIndicator.swift b/FineDust/Common/ProgressIndicator.swift new file mode 100644 index 00000000..82167264 --- /dev/null +++ b/FineDust/Common/ProgressIndicator.swift @@ -0,0 +1,86 @@ +// +// ProgressIndicator.swift +// FineDust +// +// Created by Presto on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +/// 네트워크 인디케이터 뷰 +final class ProgressIndicator: UIView { + + // MARK: Singleton Object + static let shared = ProgressIndicator(frame: UIScreen.main.bounds) + + /// 배경 뷰 + private var backgroundView: UIView! { + didSet { + backgroundView.backgroundColor = .lightGray + backgroundView.clipsToBounds = true + backgroundView.layer.cornerRadius = 10 + backgroundView.translatesAutoresizingMaskIntoConstraints = false + addSubview(backgroundView) + NSLayoutConstraint.activate([ + backgroundView.anchor.centerX.equal(to: anchor.centerX), + backgroundView.anchor.centerY.equal(to: anchor.centerY), + backgroundView.anchor.width.equal(toConstant: 100), + backgroundView.anchor.height.equal(toConstant: 100) + ]) + } + } + + // 액티비티 인디케이터 + private var indicator: UIActivityIndicatorView! { + didSet { + indicator.style = .whiteLarge + indicator.color = .black + indicator.hidesWhenStopped = true + indicator.translatesAutoresizingMaskIntoConstraints = false + backgroundView.addSubview(indicator) + NSLayoutConstraint.activate([ + indicator.anchor.centerX.equal(to: backgroundView.anchor.centerX), + indicator.anchor.centerY.equal(to: backgroundView.anchor.centerY) + ]) + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() { + backgroundColor = UIColor.black.withAlphaComponent(0.3) + backgroundView = UIView() + indicator = UIActivityIndicatorView() + } + + /// `ProgressIndicator.shared.show()`로 인디케이터 표시 + func show() { + DispatchQueue.main.async { [weak self] in + guard let `self` = self else { return } + UIApplication.shared.isNetworkActivityIndicatorVisible = true + self.indicator.startAnimating() + if let window = UIApplication.shared.keyWindow { + window.addSubview(self) + } + } + } + + /// `ProgressIndicator.shared.hide()`로 인디케이터 표시 + func hide() { + DispatchQueue.main.async { [weak self] in + guard let `self` = self else { return } + UIApplication.shared.isNetworkActivityIndicatorVisible = false + self.indicator.stopAnimating() + self.removeFromSuperview() + } + } +} diff --git a/FineDust/Core Data/CoreData+Intake.swift b/FineDust/Core Data/CoreData+Intake.swift new file mode 100644 index 00000000..9e1da133 --- /dev/null +++ b/FineDust/Core Data/CoreData+Intake.swift @@ -0,0 +1,17 @@ +// +// Intake+.swift +// FineDust +// +// Created by Presto on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// `Intake` Entity Attribute 상수 정리 +extension Intake { + /// 날짜 Attribute + static let date = "date" + /// 흡입량 Attribute + static let value = "value" +} diff --git a/FineDust/Core Data/CoreData+User.swift b/FineDust/Core Data/CoreData+User.swift new file mode 100644 index 00000000..a63b8732 --- /dev/null +++ b/FineDust/Core Data/CoreData+User.swift @@ -0,0 +1,15 @@ +// +// User+.swift +// FineDust +// +// Created by Presto on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// `User` Entity Attribute 상수 정리 +extension User { + /// 설치 날짜 Attribute + static let installedDate = "installedDate" +} diff --git a/FineDust/Core Data/FineDust.xcdatamodeld/FineDust.xcdatamodel/contents b/FineDust/Core Data/FineDust.xcdatamodeld/FineDust.xcdatamodel/contents index 476e5b6c..fb93e310 100644 --- a/FineDust/Core Data/FineDust.xcdatamodeld/FineDust.xcdatamodel/contents +++ b/FineDust/Core Data/FineDust.xcdatamodeld/FineDust.xcdatamodel/contents @@ -1,4 +1,16 @@ - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FineDust/Extension/CALayer+.swift b/FineDust/Extension/CALayer+.swift new file mode 100644 index 00000000..f4fdd7b3 --- /dev/null +++ b/FineDust/Extension/CALayer+.swift @@ -0,0 +1,59 @@ +// +// CALayer+.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +extension CALayer { + /// 경계선 관련 설정. + func setBorder( + color borderColor: UIColor = .black, + width borderWidth: CGFloat = 0, + radius cornerRadius: CGFloat = 0 + ) { + masksToBounds = true + self.borderColor = borderColor.cgColor + self.borderWidth = borderWidth + self.cornerRadius = cornerRadius + } + /// Sketch에서 제공하는 그림자 관련 정보 적용. + func applySketchShadow( + color: UIColor = .black, + alpha: Float = 0.5, + x: CGFloat = 0, + y: CGFloat = 2, + blur: CGFloat = 4, + spread: CGFloat = 0 + ) { + shadowColor = color.cgColor + shadowOpacity = alpha + shadowOffset = CGSize(width: x, height: y) + shadowRadius = blur / 2.0 + if spread == 0 { + shadowPath = nil + } else { + let dx = -spread + let rect = bounds.insetBy(dx: dx, dy: dx) + shadowPath = UIBezierPath(rect: rect).cgPath + } + } + /// 그라데이션 효과 적용. + func applyGradient( + colors: [Any], + locations: [NSNumber], + startPoint: CGPoint = .init(x: 0.5, y: 0), + endPoint: CGPoint = .init(x: 0.5, y: 1) + ) { + let gradient = CAGradientLayer() + gradient.frame = bounds + gradient.startPoint = startPoint + gradient.endPoint = endPoint + gradient.colors = colors + gradient.locations = locations + mask = gradient + } +} diff --git a/FineDust/Extension/Date+.swift b/FineDust/Extension/Date+.swift new file mode 100644 index 00000000..43985645 --- /dev/null +++ b/FineDust/Extension/Date+.swift @@ -0,0 +1,58 @@ +// +// Date+.swift +// FineDust +// +// Created by Presto on 21/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +extension Date { + /// 기준 날짜 이전의 `Date` 구하기. + static func before(days: Int, since date: Date = Date()) -> Date { + return Calendar.current.date(byAdding: .day, value: -days, to: date) ?? Date() + } + + /// 기준 날짜 이후의 `Date` 구하기. + static func after(days: Int, since date: Date = Date()) -> Date { + return Calendar.current.date(byAdding: .day, value: days, to: date) ?? Date() + } + + /// 이전 `Date` 구하기. + func before(days: Int) -> Date { + return Calendar.current.date(byAdding: .day, value: -days, to: self) ?? Date() + } + + /// 이후 `Date` 구하기. + func after(days: Int) -> Date { + return Calendar.current.date(byAdding: .day, value: days, to: self) ?? Date() + } + + /// 기준 날짜의 0시 `Date` 구하기. + static func start(of date: Date = Date()) -> Date { + return Calendar.current.startOfDay(for: date) + } + + /// 기준 날짜의 23시 59분 59초 `Date` 구하기. + static func end(of date: Date = Date()) -> Date { + let components = DateComponents(day: 1, second: -1) + return Calendar.current.date(byAdding: components, to: start(of: date)) ?? Date() + } + + /// 0시 `Date` 구하기. + var start: Date { + return Calendar.current.startOfDay(for: self) + } + + /// 23시 59분 59초 `Date` 구하기. + var end: Date { + let components = DateComponents(day: 1, second: -1) + return Calendar.current.date(byAdding: components, to: start) ?? Date() + } + + /// 주어진 날짜가 오늘인지 구하기. + var isToday: Bool { + return Calendar.current.isDateInToday(self) + } +} diff --git a/FineDust/Extension/Double+.swift b/FineDust/Extension/Double+.swift new file mode 100644 index 00000000..a800f7d7 --- /dev/null +++ b/FineDust/Extension/Double+.swift @@ -0,0 +1,16 @@ +// +// Double+.swift +// FineDust +// +// Created by 이재은 on 28/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +extension Double { + /// kilometer 변환하기. + var kilometer: Double { + return self / 1000 + } +} diff --git a/FineDust/Extension/NSLayoutAnchor+.swift b/FineDust/Extension/NSLayoutAnchor+.swift new file mode 100644 index 00000000..9be798e1 --- /dev/null +++ b/FineDust/Extension/NSLayoutAnchor+.swift @@ -0,0 +1,30 @@ +// +// NSLayoutAnchor+.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +extension NSLayoutAnchor { + /// `constraint(equalTo:constant:)` 메소드의 Helper. + @objc func equal(to anchor: NSLayoutAnchor, offset: CGFloat = 0) -> NSLayoutConstraint { + return constraint(equalTo: anchor, constant: offset) + } + /// `constraint(greaterThanOrEqualTo:constant:)` 메소드의 Helper. + @objc func greaterThanOrEqual( + to anchor: NSLayoutAnchor, + offset: CGFloat = 0 + ) -> NSLayoutConstraint { + return constraint(greaterThanOrEqualTo: anchor, constant: offset) + } + /// `constraint(lessThanOrEqualTo:constant:)` 메소드의 Helper. + @objc func lessThanOrEqual( + to anchor: NSLayoutAnchor, + offset: CGFloat = 0 + ) -> NSLayoutConstraint { + return constraint(lessThanOrEqualTo: anchor, constant: offset) + } +} diff --git a/FineDust/Extension/NSLayoutConstraint+.swift b/FineDust/Extension/NSLayoutConstraint+.swift new file mode 100644 index 00000000..c42b24ee --- /dev/null +++ b/FineDust/Extension/NSLayoutConstraint+.swift @@ -0,0 +1,30 @@ +// +// NSLayoutConstraint+.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +extension NSLayoutConstraint { + /// multiplier 변경하기. + func changedMultiplier(to value: CGFloat) -> NSLayoutConstraint { + let constraint = NSLayoutConstraint( + item: firstItem as Any, + attribute: firstAttribute, + relatedBy: relation, + toItem: secondItem, + attribute: secondAttribute, + multiplier: value, + constant: constant + ) + constraint.priority = priority + constraint.shouldBeArchived = shouldBeArchived + constraint.identifier = identifier + NSLayoutConstraint.deactivate([self]) + NSLayoutConstraint.activate([constraint]) + return constraint + } +} diff --git a/FineDust/Extension/NSLayoutDimension+.swift b/FineDust/Extension/NSLayoutDimension+.swift new file mode 100644 index 00000000..9f5a17b8 --- /dev/null +++ b/FineDust/Extension/NSLayoutDimension+.swift @@ -0,0 +1,16 @@ +// +// NSLayoutDimension+.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +extension NSLayoutDimension { + /// `constraint(equalToConstant:)` 메소드의 Helper. + func equal(toConstant offset: CGFloat) -> NSLayoutConstraint { + return constraint(equalToConstant: offset) + } +} diff --git a/FineDust/Extension/NSObject+.swift b/FineDust/Extension/NSObject+.swift index ba125a29..dbbe9e99 100644 --- a/FineDust/Extension/NSObject+.swift +++ b/FineDust/Extension/NSObject+.swift @@ -8,11 +8,12 @@ import Foundation -public extension NSObject { +extension NSObject { + /// 클래스 이름을 문자열로 변환. var classNameToString: String { return NSStringFromClass(type(of: self)) } - + /// 클래스 이름을 문자열로 변환. static var classNameToString: String { return NSStringFromClass(self).components(separatedBy: ".").last ?? "" } diff --git a/FineDust/Extension/Notification.Name+.swift b/FineDust/Extension/Notification.Name+.swift new file mode 100644 index 00000000..ad963893 --- /dev/null +++ b/FineDust/Extension/Notification.Name+.swift @@ -0,0 +1,22 @@ +// +// Notification.Name+.swift +// FineDust +// +// Created by Presto on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +extension Notification.Name { + /// 관측소 조회 통신이 성공했을 때의 노티피케이션 이름. + static let fetchObservatoryDidSuccess = Notification.Name("fetchObservatoryDidSuccess") + /// 미세먼지 농도 조회 통신이 성공했을 때의 노티피케이션 이름. + static let fetchFineDustConcentrationDidSuccess + = Notification.Name("fetchFineDustConcentrationDidSuccess") + /// 관측소 조회 통신이 실패했을 때의 노티피케이션 이름. + static let fetchObservatoryDidError = Notification.Name("fetchObservatoryDidError") + /// 미세먼지 농도 조회 통신이 실패했을 때의 노티피케이션 이름. + static let fetchFineDustConcentrationDidError + = Notification.Name("fetchFineDustConcentrationDidError") +} diff --git a/FineDust/Extension/String+.swift b/FineDust/Extension/String+.swift index e544b9ad..eb319f4a 100644 --- a/FineDust/Extension/String+.swift +++ b/FineDust/Extension/String+.swift @@ -9,6 +9,11 @@ import Foundation extension String { + /// 문자열 로컬라이징. + var localized: String { + return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") + } + /// 문자열 퍼센트 인코딩. var percentEncoded: String { return addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" } diff --git a/FineDust/Extension/UIAlertController+.swift b/FineDust/Extension/UIAlertController+.swift index 501459b4..e7f0b684 100644 --- a/FineDust/Extension/UIAlertController+.swift +++ b/FineDust/Extension/UIAlertController+.swift @@ -8,7 +8,8 @@ import UIKit -public extension UIAlertController { +extension UIAlertController { + /// `UIAlertController` Helper. static func alert( title: String?, message: String?, @@ -17,13 +18,13 @@ public extension UIAlertController { let alert = UIAlertController(title: title, message: message, preferredStyle: style) return alert } - + /// `addTextField(_:)` Helper. @discardableResult func textField(_ configuration: ((UITextField) -> Void)? = nil) -> UIAlertController { addTextField(configurationHandler: configuration) return self } - + /// `UIAlertAction` Helper. @discardableResult func action( title: String?, @@ -39,7 +40,7 @@ public extension UIAlertController { addAction(action) return self } - + /// 빌더 패턴을 통해 만들어진 `UIAlertController` present. func present( to viewController: UIViewController?, animated: Bool = true, diff --git a/FineDust/Extension/UIColor+.swift b/FineDust/Extension/UIColor+.swift index 8534ece9..1f0c1a8b 100644 --- a/FineDust/Extension/UIColor+.swift +++ b/FineDust/Extension/UIColor+.swift @@ -9,11 +9,26 @@ import UIKit extension UIColor { + /// RGB 값으로 색상 만들기. convenience init(red: CGFloat, green: CGFloat, blue: CGFloat) { self.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1) } - + /// RGB 값이 같을 때 색상 만들기. convenience init(rgb: CGFloat) { self.init(red: rgb, green: rgb, blue: rgb) } + /// HEX 값으로 색상 만들기. + convenience init(red: Int, green: Int, blue: Int) { + self.init( + red: CGFloat(red) / 255, + green: CGFloat(green) / 255, + blue: CGFloat(blue) / 255, + alpha: 1 + ) + } + /// HEX 값이 같을 때 색상 만들기. + convenience init(rgb: Int) { + self.init(red: (rgb >> 16) & 0xFF, green: (rgb >> 8) & 0xFF, blue: rgb & 0xFF + ) + } } diff --git a/FineDust/Extension/UIImageView+.swift b/FineDust/Extension/UIImageView+.swift new file mode 100644 index 00000000..dc315235 --- /dev/null +++ b/FineDust/Extension/UIImageView+.swift @@ -0,0 +1,16 @@ +// +// UIImageView+.swift +// FineDust +// +// Created by 이재은 on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +extension UIImageView { + func setRounded() { + layer.cornerRadius = frame.height / 2 + layer.masksToBounds = true + } +} diff --git a/FineDust/Extension/UIView+.swift b/FineDust/Extension/UIView+.swift index e1c30ac1..0123bfd7 100644 --- a/FineDust/Extension/UIView+.swift +++ b/FineDust/Extension/UIView+.swift @@ -8,8 +8,9 @@ import UIKit -public extension UIView { - static func create(fromXib name: String) -> UIView? { +extension UIView { + /// `UIView` instantiate. + static func instantiate(fromXib name: String) -> UIView? { return UINib(nibName: name, bundle: nil) .instantiate(withOwner: nil, options: nil).first as? UIView } diff --git a/FineDust/Extension/UIView+NSLayoutAnchor.swift b/FineDust/Extension/UIView+NSLayoutAnchor.swift new file mode 100644 index 00000000..be1936c9 --- /dev/null +++ b/FineDust/Extension/UIView+NSLayoutAnchor.swift @@ -0,0 +1,38 @@ +// +// UIView+NSLayoutAnchor.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +extension UIView { + /// anchor 정보를 담는 구조체. + struct Anchor { + let view: UIView + let top: NSLayoutYAxisAnchor + let bottom: NSLayoutYAxisAnchor + let leading: NSLayoutXAxisAnchor + let trailing: NSLayoutXAxisAnchor + let centerX: NSLayoutXAxisAnchor + let centerY: NSLayoutYAxisAnchor + let width: NSLayoutDimension + let height: NSLayoutDimension + } + /// `UIView`의 `Anchor` 정보. + var anchor: Anchor { + return Anchor( + view: self, + top: topAnchor, + bottom: bottomAnchor, + leading: leadingAnchor, + trailing: trailingAnchor, + centerX: centerXAnchor, + centerY: centerYAnchor, + width: widthAnchor, + height: heightAnchor + ) + } +} diff --git a/FineDust/Extension/UIViewController+.swift b/FineDust/Extension/UIViewController+.swift index ff685d5e..b9fb8c90 100644 --- a/FineDust/Extension/UIViewController+.swift +++ b/FineDust/Extension/UIViewController+.swift @@ -8,19 +8,23 @@ import UIKit -public extension UIViewController { - static func create(fromStoryboard storyboard: String, identifier: String) -> UIViewController { +extension UIViewController { + /// `UIViewController` instantiate. + static func instantiate( + fromStoryboard storyboard: String, + identifier: String + ) -> UIViewController { let storyboard = UIStoryboard(name: storyboard, bundle: nil) let controller = storyboard.instantiateViewController(withIdentifier: identifier) return controller } - + /// 클로저 내에서 해당 타입으로 캐스팅하여 구성하기. @discardableResult - func deliver(_ closure: (UIViewController) -> Void) -> UIViewController { - closure(self) + func configure(_ configureHandler: (UIViewController) -> Void) -> UIViewController { + configureHandler(self) return self } - + /// 빌더 패턴을 통해 만들어진 `UIViewController`를 모달 present. func present( to viewController: UIViewController, transitionStyle style: UIModalTransitionStyle = .coverVertical, @@ -30,8 +34,12 @@ public extension UIViewController { modalTransitionStyle = style viewController.present(self, animated: animated, completion: completion) } - - func push(at navigationController: UINavigationController?, animated: Bool = true) { - navigationController?.pushViewController(self, animated: animated) + /// 빌더 패턴을 통해 만들어진 `UIViewController`를 내비게이션 스택에 추가. + func push(at viewController: UIViewController?, animated: Bool = true) { + if let navigationController = viewController?.navigationController { + navigationController.pushViewController(self, animated: animated) + } else { + fatalError("해당 ViewController는 Navigation 스택에 있지 않습니다.") + } } } diff --git a/FineDust/Feedback/Controller/FeedbackListViewController.swift b/FineDust/Feedback/Controller/FeedbackListViewController.swift new file mode 100644 index 00000000..1222e563 --- /dev/null +++ b/FineDust/Feedback/Controller/FeedbackListViewController.swift @@ -0,0 +1,98 @@ +// +// FeedbackListViewController.swift +// FineDust +// +// Created by 이재은 on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +final class FeedbackListViewController: UIViewController { + + // MARK: IBOutlet + @IBOutlet private weak var feedbackCollectionView: UICollectionView! + @IBOutlet private weak var feedbackListTabelView: UITableView! + + // MARK: Properties + private let reuseIdentifiers = ["feedbackCell", "feedbackListCell"] + private var count = 10 + private let cornerRadius: CGFloat = 5 + private let screenSize = UIScreen.main.bounds + + // MARK: - LifeCycle + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "먼지 정보" + + feedbackCollectionView.reloadData() + feedbackListTabelView.reloadData() + } +} + +// MARK: - UICollectionViewDataSource + +extension FeedbackListViewController: UICollectionViewDataSource { + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + func collectionView( + _ collectionView: UICollectionView, + numberOfItemsInSection section: Int + ) -> Int { + return 3 + } + + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: reuseIdentifiers[0], + for: indexPath + ) as? FeedbackCollectionViewCell + else { return UICollectionViewCell() } + + cell.setProperties() + // cell.feedbackTitleLabel.text = "미세먼지 정화 식물" + // cell.feedbackTitleLabel.layer.cornerRadius = cornerRadius + // cell.feedbackTitleLabel.layer.masksToBounds = true + + return cell + } +} + +// MARK: - UITabelViewDataSource + +extension FeedbackListViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: reuseIdentifiers[1], + for: indexPath + ) as? FeedbackListTableViewCell else { + return UITableViewCell() + } + + cell.setProperties(at: indexPath.row) + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension FeedbackListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 165 + } +} diff --git a/FineDust/Feedback/Feedback.storyboard b/FineDust/Feedback/Feedback.storyboard new file mode 100644 index 00000000..87e40f5a --- /dev/null +++ b/FineDust/Feedback/Feedback.storyboard @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 틸란드시아는 공기 중에 있는 수분과 먼지를 흡수합니다. 자일렌 제거량이 ‘최상’이고, 포름알데히드 제거량은 ‘상’등급으로 평가될 정도로 우수한 공기정화식물입니다. 이처럼 포름알데히드와 자일렌 등의 새집 증후군 원인 물질 제거 효과에 우수하기 때문에 거실에 놓을 경우 새집증후군 완화 효과가 탁월합니다. 

 틸란드시아는 착생식물로, 나무 같은 곳에 착생하여 공중에 매달려 살기 때문에 ‘공중 식물’이라고도 불리기도 합니다.

<키우는 법> + 흙이나 물 없이 걸어두거나 수반 등에 얹어 키울 수 있습니다. 반음지에서도 잘 자라지만, 꽃을 피우거나 오래 키우기 위해서는 평소보다 많은 양의 햇빛이 필요합니다. 하지만 직사광선은 피해야 합니다. +
 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FineDust/Feedback/View/FeedbackCollectionViewCell.swift b/FineDust/Feedback/View/FeedbackCollectionViewCell.swift new file mode 100644 index 00000000..d43fe18a --- /dev/null +++ b/FineDust/Feedback/View/FeedbackCollectionViewCell.swift @@ -0,0 +1,21 @@ +// +// FeedbackCollectionViewCell.swift +// FineDust +// +// Created by 이재은 on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +final class FeedbackCollectionViewCell: UICollectionViewCell { + + @IBOutlet private weak var feedbackImageView: UIImageView! + @IBOutlet private weak var feedbackTitleLabel: UILabel! + + func setProperties() { + feedbackImageView.layer.cornerRadius = 5 + feedbackImageView.layer.masksToBounds = true + feedbackImageView.image = UIImage(named: "info1") + } +} diff --git a/FineDust/Feedback/View/FeedbackListTableViewCell.swift b/FineDust/Feedback/View/FeedbackListTableViewCell.swift new file mode 100644 index 00000000..5b296710 --- /dev/null +++ b/FineDust/Feedback/View/FeedbackListTableViewCell.swift @@ -0,0 +1,37 @@ +// +// FeedbackListTableViewCell.swift +// FineDust +// +// Created by 이재은 on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +final class FeedbackListTableViewCell: UITableViewCell { + + @IBOutlet private weak var feedbackImageView: UIImageView! + @IBOutlet private weak var feedbackTitleLabel: UILabel! + @IBOutlet private weak var feedbackSourceLabel: UILabel! + @IBOutlet private weak var feedbackListShadowView: UIView! + @IBOutlet private weak var feedbackListTitleLabel: UILabel! + @IBOutlet private weak var bookmarkButton: UIButton! + + func setProperties(at index: Int) { + feedbackListTitleLabel.isHidden = index != 0 ? true : false + feedbackImageView.image = UIImage(named: "info1") + feedbackTitleLabel.text = "미세먼지 정화 식물" + feedbackSourceLabel.text = "KTV 국민 방송" + + feedbackImageView.setRounded() + feedbackListShadowView.layer.applySketchShadow( + color: UIColor.gray, + alpha: 0.2, + x: 2, + y: 2, + blur: 5, + spread: 3 + ) + feedbackListShadowView.layer.cornerRadius = 5 + } +} diff --git a/FineDust/FineDust.entitlements b/FineDust/FineDust.entitlements new file mode 100644 index 00000000..5d9b5a0b --- /dev/null +++ b/FineDust/FineDust.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + health-records + + + diff --git a/FineDust/HealthKit/FineDustHK.swift b/FineDust/HealthKit/FineDustHK.swift new file mode 100644 index 00000000..d01c0efb --- /dev/null +++ b/FineDust/HealthKit/FineDustHK.swift @@ -0,0 +1,88 @@ +// +// FineDustHK.swift +// FineDust +// +// Created by zun on 24/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit +import HealthKit + +final class FineDustHK: OpenHealthDelegate { + + // MARK: - Properties + + static let shared = FineDustHK() + + ///Health 앱 데이터 권한을 요청하기 위한 프로퍼티 + private let healthStore = HKHealthStore() + ///Health 앱 데이터 중 걸음 수를 가져오기 위한 프로퍼티 + private let stepCount = HKObjectType.quantityType( + forIdentifier: HKQuantityTypeIdentifier.stepCount + ) + ///Health 앱 데이터 중 걸은 거리를 가져오기 위한 프로퍼티 + private let distance = HKObjectType.quantityType( + forIdentifier: HKQuantityTypeIdentifier.distanceWalkingRunning + ) + ///Health App 권한을 나타내는 변수 + private var isAuthorized = true + + // MARK: - Methods + + private init() { } + + func requestAuthorization() { + guard let stepCount = stepCount else { + print("step count request error") + return + } + guard let distance = distance else { + print("distance request error") + return + } + //권한이 없을 경우 사용자가 직접 허용을 해야하게끔 해주기 위해 변수를 false로 설정 + if healthStore.authorizationStatus(for: stepCount) == .sharingDenied + || healthStore.authorizationStatus(for: distance) == .sharingDenied { + isAuthorized = false + return + } + + //걸음 데이터를 얻기 위해 Set을 만든 다음 권한 요청. + let healthKitTypes: Set = [stepCount, distance] + + healthStore.requestAuthorization( + toShare: healthKitTypes, + read: healthKitTypes + ) { _, error in + if let err = error { + print("request authorization error : \(err.localizedDescription)") + } else { + print("complete request authorization") + } + } + } + + //권한이 없을경우 건강 App으로 이동시키는 메소드 + func openHealth(_ viewController: UIViewController) { + if !isAuthorized { + //이 코드로 인해 alert가 1번만 뜨게된다. + isAuthorized = true + UIAlertController + .alert( + title: "건강 App에 대한 권한이 없습니다.", + message: "App을 이용하려면 건강 App에 대한 권한이 필요합니다. 건강 -> 3번째 탭 데이터 소스 -> FineDust -> 권한허용을 해주세요" + ) + .action( + title: "건강 App", + style: .default + ) { _, _ in + UIApplication.shared.open(URL(string: "x-apple-health://")!) + } + .action( + title: "취소", style: .cancel, handler: nil + ) + .present(to: viewController) + } + } +} diff --git a/FineDust/HealthKit/HealthKitManager.swift b/FineDust/HealthKit/HealthKitManager.swift new file mode 100644 index 00000000..03c95066 --- /dev/null +++ b/FineDust/HealthKit/HealthKitManager.swift @@ -0,0 +1,101 @@ +// +// HealthKitManager.swift +// FineDust +// +// Created by 이재은 on 28/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation +import HealthKit +/// HealthKitManagerType 프로토콜 선언 +protocol HealthKitManagerType { + func findHealthKitValue( + startDate: Date, + endDate: Date, + quantityFor: HKUnit, + quantityTypeIdentifier: HKQuantityTypeIdentifier, + completion: @escaping (Double) -> Void) + func fetchDistanceValue(_ completion: @escaping (Double) -> Void) + func fetchStepCountValue(_ completion: @escaping (Double) -> Void) +} +/// HealthKitManager 기능 구현 부분 +struct HealthKitManager: HealthKitManagerType { + // HealthKit Data에 접근하는 지점. + private let healthStore = HKHealthStore() + private let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let startDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()) + let endDate = Date() + /// HealthKit 값 가져오는 함수. + func findHealthKitValue( + startDate: Date, + endDate: Date, + quantityFor: HKUnit, + quantityTypeIdentifier: HKQuantityTypeIdentifier, + completion: @escaping (Double) -> Void + ) { + if let quantityType = HKQuantityType.quantityType(forIdentifier: quantityTypeIdentifier) { + // 시작 및 끝 날짜가 지정된 시간 간격 내에 있는 샘플에 대한 서술을 반환함 + let predicate = HKQuery.predicateForSamples( + withStart: startDate, + end: endDate, + options: .strictStartDate) + // 가져올 날짜 단위 변수. + var interval: DateComponents = DateComponents() + interval.day = 1 + // 정한 시간에 대한 통계 쿼리를 수행하고 결과를 반환함. + let query = HKStatisticsCollectionQuery( + quantityType: quantityType, + quantitySamplePredicate: predicate, + options: [.cumulativeSum], + anchorDate: startDate, + intervalComponents: interval + ) + query.initialResultsHandler = { query, results, error in + if error != nil { + print("findHealthKitValue error: \(String(describing: error?.localizedDescription))") + return + } + if let results = results { + if results.statistics().count == 0 { + completion(0) + } else { + // 시작 날짜부터 종료 날짜까지의 모든 시간 간격에 대한 통계 개체를 나열함. + results.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in + // 쿼리와 일치하는 모든 값을 더함. + if let quantity = statistics.sumQuantity() { + let quantityValue = quantity.doubleValue(for: quantityFor) + completion(quantityValue) + } + } + } + } else { + print("HKStatisticsCollectionQuery failed!") + } + } + healthStore.execute(query) + } + } + /// 걸은 거리 가져오기. + func fetchDistanceValue(_ completion: @escaping (Double) -> Void) { + guard let startDate = startDate else { return } + findHealthKitValue( + startDate: startDate, + endDate: endDate, + quantityFor: HKUnit.meter(), + quantityTypeIdentifier: .distanceWalkingRunning, + completion: completion + ) + } + /// 걸음 수 가져오기. + func fetchStepCountValue(_ completion: @escaping (Double) -> Void) { + guard let startDate = startDate else { return } + findHealthKitValue( + startDate: startDate, + endDate: endDate, + quantityFor: HKUnit.count(), + quantityTypeIdentifier: .stepCount, + completion: completion + ) + } +} diff --git a/FineDust/HealthKit/OpenHealthDelegate.swift b/FineDust/HealthKit/OpenHealthDelegate.swift new file mode 100644 index 00000000..fa9cc192 --- /dev/null +++ b/FineDust/HealthKit/OpenHealthDelegate.swift @@ -0,0 +1,14 @@ +// +// OpenHealthDelegate.swift +// FineDust +// +// Created by zun on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +///건강 App으로 이동시키는 기능을 모듈화 하기 위한 프로토콜 +protocol OpenHealthDelegate: class { + func openHealth(_ viewController: UIViewController) +} diff --git a/FineDust/Info.plist b/FineDust/Info.plist index 62b49beb..b84be41c 100644 --- a/FineDust/Info.plist +++ b/FineDust/Info.plist @@ -20,14 +20,29 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSHealthShareUsageDescription + 사용자의 건강 데이터를 사용하여 미세먼지 섭취량을 계산합니다. + NSHealthUpdateUsageDescription + 사용자의 건강 데이터를 사용하여 미세먼지 섭취량을 계산합니다. + NSLocationAlwaysAndWhenInUseUsageDescription + 위치 정보를 사용하여 현재 위치의 미세먼지 농도를 가져옵니다. + NSLocationWhenInUseUsageDescription + 위치 정보를 사용하여 현재 위치의 미세먼지 농도를 가져옵니다. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile - Main + Common UIRequiredDeviceCapabilities armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -39,5 +54,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIViewControllerBasedStatusBarAppearance + diff --git a/FineDust/Main/Base.lproj/Main.storyboard b/FineDust/Main/Base.lproj/Main.storyboard new file mode 100644 index 00000000..738d63e5 --- /dev/null +++ b/FineDust/Main/Base.lproj/Main.storyboard @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FineDust/Main/Controller/MainViewController.swift b/FineDust/Main/Controller/MainViewController.swift new file mode 100644 index 00000000..7853818d --- /dev/null +++ b/FineDust/Main/Controller/MainViewController.swift @@ -0,0 +1,61 @@ +// +// ViewController.swift +// FineDust +// +// Created by Presto on 21/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import CoreLocation +import UIKit + +final class MainViewController: UIViewController { + + // MARK: - IBOutlets + + @IBOutlet private weak var distanceLabel: UILabel! + @IBOutlet private weak var stepCountLabel: UILabel! + + // MARK: - Properties + + weak var delegate: OpenHealthDelegate? + private var healthKitManager = HealthKitManager() + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + putHealthKitValue() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + delegate?.openHealth(self) + } +} + +// MARK: - Functions + +extension MainViewController { + private func setup() { + delegate = FineDustHK.shared + } + + private func putHealthKitValue() { + healthKitManager.fetchDistanceValue { value in + DispatchQueue.main.async { + self.distanceLabel.text = String(format: "%.1f", value.kilometer) + " km" + } + } + healthKitManager.fetchStepCountValue { value in + DispatchQueue.main.async { + self.stepCountLabel.text = "\(Int(value)) 걸음" + } + } + } +} diff --git a/FineDust/MainViewController.swift b/FineDust/MainViewController.swift deleted file mode 100644 index 2ae08486..00000000 --- a/FineDust/MainViewController.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// MainViewController.swift -// FineDust -// -// Created by Presto on 21/01/2019. -// Copyright © 2019 boostcamp3rd. All rights reserved. -// - -import Foundation diff --git a/FineDust/Model/FineDustInfo.swift b/FineDust/Model/FineDustInfo.swift new file mode 100644 index 00000000..62493781 --- /dev/null +++ b/FineDust/Model/FineDustInfo.swift @@ -0,0 +1,54 @@ +// +// FineDustInfo.swift +// FineDust +// +// Created by Presto on 25/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// 미세먼지 정보를 담는 싱글톤 객체. +final class FineDustInfo { + + // MARK: Singleton Object + + static let shared = FineDustInfo() + + // MARK: Properties + + private var stationName: String = "" + + private var fineDustResponse: FineDustResponse? { + // API 호출 후 미세먼지 응답을 싱글톤에 담을 때마다 노티피케이션을 쏴줌. + didSet { + NotificationCenter.default.post( + name: .fetchFineDustConcentrationDidSuccess, + object: nil, + userInfo: ["data": fineDustResponse as Any] + ) + } + } + + /// 관측소. + var observatory: String { + return stationName + } + + /// 미세먼지 응답. + var response: FineDustResponse? { + return fineDustResponse + } + + // MARK: Methods + + /// 관측소 정보 설정. + func set(observatory: String) { + stationName = observatory + } + + /// 응답 정보 설정. + func set(fineDustResponse response: FineDustResponse?) { + fineDustResponse = response + } +} diff --git a/FineDust/Model/GeoInfo.swift b/FineDust/Model/GeoInfo.swift new file mode 100644 index 00000000..3319bb48 --- /dev/null +++ b/FineDust/Model/GeoInfo.swift @@ -0,0 +1,46 @@ +// +// GeoInfo.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// 좌표 정보를 담는 싱글톤 객체. +final class GeoInfo { + + // MARK: Singleton Object + + /// 좌표 정보의 싱글톤 객체 + static let shared = GeoInfo() + + // MARK: Private Initializer + + private init() { } + + // MARK: Properties + + private var xLocation: Double = 0 + + private var yLocation: Double = 0 + + /// X 좌표 + var x: Double { + return xLocation + } + + /// Y 좌표 + var y: Double { + return yLocation + } + + // MARK: Methods + + /// 좌표 설정 + func setLocation(x: Double, y: Double) { + xLocation = x + yLocation = y + } +} diff --git a/FineDust/Network/HTTPMethod.swift b/FineDust/Network/HTTPMethod.swift deleted file mode 100644 index 497de7f6..00000000 --- a/FineDust/Network/HTTPMethod.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// HTTPMethod.swift -// FineDust -// -// Created by Presto on 21/01/2019. -// Copyright © 2019 boostcamp3rd. All rights reserved. -// - -import Foundation - -enum HTTPMethod: String { - - case get = "GET" - - case post = "POST" -} diff --git a/FineDust/Network/Network.swift b/FineDust/Network/Network.swift index c5c8bc86..80a76f38 100644 --- a/FineDust/Network/Network.swift +++ b/FineDust/Network/Network.swift @@ -8,25 +8,45 @@ import Foundation +/// 네트워크 요청 관련 클래스. final class Network { - + /// HTTP 메소드를 정의한 열거형. + enum HTTPMethod: String { + /// GET 메소드. + case get = "GET" + /// POST 메소드. + case post = "POST" + } + /// 네트워크 요청. + /// + /// - Parameters: + /// - url: URL. + /// - method: HTTP Method. + /// - parameters: HTTP Body에 들어갈 키/값 쌍. 기본값은 `[:]`. + /// - headers: HTTP Header에 들어갈 키/값 쌍. 기본값은 `[:]`. + /// - completion: 컴플리션 핸들러. class func request( _ url: URL, method: HTTPMethod, - parameters: [String: Any] = [:], + parameters: [String: Any]? = nil, headers: [String: String] = [:], - completion: @escaping (Data?, Int?, Error?) -> Void + completion: @escaping (Data?, Error?) -> Void ) { let session = URLSession(configuration: .default) var urlRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue - guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) - else { return } - urlRequest.httpBody = httpBody + if let parameters = parameters { + urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) + } headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } - let task = session.dataTask(with: urlRequest) { data, response, error in - guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return } - completion(data, statusCode, error) + DispatchQueue.main.async { + ProgressIndicator.shared.show() + } + let task = session.dataTask(with: urlRequest) { data, _, error in + completion(data, error) + DispatchQueue.main.async { + ProgressIndicator.shared.hide() + } session.finishTasksAndInvalidate() } task.resume() diff --git a/FineDust/Response/DataTerm.swift b/FineDust/Response/DataTerm.swift new file mode 100644 index 00000000..03c342de --- /dev/null +++ b/FineDust/Response/DataTerm.swift @@ -0,0 +1,15 @@ +// +// DataTerm.swift +// FineDust +// +// Created by Presto on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +/// 실시간 미세먼지 정보 얻어오는 기간 정의 +enum DataTerm: String { + + case daily + + case month +} diff --git a/FineDust/Response/FineDustResponse.swift b/FineDust/Response/FineDustResponse.swift new file mode 100644 index 00000000..119ef77e --- /dev/null +++ b/FineDust/Response/FineDustResponse.swift @@ -0,0 +1,113 @@ +// +// FineDustResponse.swift +// FineDust +// +// Created by Presto on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// 미세먼지 정보 응답 객체. +struct FineDustResponse: Codable { + + struct List: Codable { + + let dataTime: String + + /// 미세먼지 농도 + private let fineDustValueResponse: String + + /// 미세먼지 24시간 예측 이동 농도 + private let fineDustValue24Response: String + + /// 미세먼지 24시간 등급 + private let fineDustGradeResponse: String + + /// 미세먼지 1시간 등급 + private let fineDustGrade1hResponse: String + + /// 초미세먼지 농도 + private let ultraFineDustValueResponse: String + + /// 초미세먼지 24시간 예측 이동 농도 + private let ultraFineDustValue24Response: String + + /// 초미세먼지 24시간 등급 + private let ultraFineDustGradeResponse: String + + /// 초미세먼지 1시간 등급 + private let ultraFineDustGrade1hResponse: String + + enum CodingKeys: String, CodingKey { + + case dataTime + + case fineDustValueResponse = "pm10Value" + + case fineDustValue24Response = "pm10Value24" + + case fineDustGradeResponse = "pm10Grade" + + case fineDustGrade1hResponse = "pm10Grade1h" + + case ultraFineDustValueResponse = "pm25Value" + + case ultraFineDustValue24Response = "pm25Value24" + + case ultraFineDustGradeResponse = "pm25Grade" + + case ultraFineDustGrade1hResponse = "pm25Grade1h" + } + + /// 미세먼지 농도 + var fineDustValue: Int { + return Int(fineDustValueResponse) ?? 0 + } + + /// 미세먼지 24시간 예측 이동 농도 + var fineDustValue24: Int { + return Int(fineDustValue24Response) ?? 0 + } + + /// 미세먼지 24시간 등급 + var fineDustGrade: Int { + return Int(fineDustGradeResponse) ?? 0 + } + + /// 미세먼지 1시간 등급 + var fineDustGrade1h: Int { + return Int(fineDustGrade1hResponse) ?? 0 + } + + /// 초미세먼지 농도 + var ultraFineDustValue: Int { + return Int(ultraFineDustValueResponse) ?? 0 + } + + /// 초미세먼지 24시간 예측 이동 농도 + var ultraFineDustValue24: Int { + return Int(ultraFineDustValue24Response) ?? 0 + } + + /// 초미세먼지 24시간 등급 + var ultraFineDustGrade: Int { + return Int(ultraFineDustGradeResponse) ?? 0 + } + + /// 초미세먼지 1시간 등급 + var ultraFineDustGrade1h: Int { + return Int(ultraFineDustGrade1hResponse) ?? 0 + } + } + + let list: [List] + + /// 응답 개수 + let totalCount: Int + + /// 서브스크립트로 리스트의 값에 접근. `response[1]` + subscript(index: Int) -> List { + return list[index] + } +} diff --git a/FineDust/Response/Grade.swift b/FineDust/Response/Grade.swift new file mode 100644 index 00000000..37c44126 --- /dev/null +++ b/FineDust/Response/Grade.swift @@ -0,0 +1,19 @@ +// +// Grade.swift +// FineDust +// +// Created by Presto on 23/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +/// 미세먼지 API 응답에 의한 대기 등급 정의 +enum Grade: Int { + + case good = 1 + + case normal + + case bad + + case veryBad +} diff --git a/FineDust/Response/ObservatoryResponse.swift b/FineDust/Response/ObservatoryResponse.swift new file mode 100644 index 00000000..0c29b1a0 --- /dev/null +++ b/FineDust/Response/ObservatoryResponse.swift @@ -0,0 +1,43 @@ +// +// ObservatoryResponse.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import Foundation + +/// 측정소 정보 조회 응답 객체. +struct ObservatoryResponse: Codable { + + struct List: Codable { + + /// 관측소 주소 + let address: String + + /// 관측소 이름 + let observatory: String + + /// 현재 위치에서 관측소까지의 거리. km + let distance: Double + + enum CodingKeys: String, CodingKey { + + case address = "addr" + + case distance = "tm" + + case observatory = "stationName" + } + } + + let list: [List] + + /// 응답 개수 + let totalCount: Int + + var observatory: String? { + return list.first?.observatory + } +} diff --git a/FineDust/Statistics/Controller/StatisticsDatePickerViewController.swift b/FineDust/Statistics/Controller/StatisticsDatePickerViewController.swift new file mode 100644 index 00000000..5a7d9356 --- /dev/null +++ b/FineDust/Statistics/Controller/StatisticsDatePickerViewController.swift @@ -0,0 +1,22 @@ +// +// StatisticsDatePickerViewController.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +final class StatisticsDatePickerViewController: UIViewController { + + @IBOutlet private weak var datePicker: UIDatePicker! { + didSet { + datePicker.maximumDate = Date() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/FineDust/Statistics/Controller/StatisticsViewController.swift b/FineDust/Statistics/Controller/StatisticsViewController.swift new file mode 100644 index 00000000..02002137 --- /dev/null +++ b/FineDust/Statistics/Controller/StatisticsViewController.swift @@ -0,0 +1,183 @@ +// +// StatisticsViewController.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +/// 통계 관련 뷰 컨트롤러. +final class StatisticsViewController: UIViewController { + + /// CALayer 관련 상수 정의. + enum Layer { + /// 경계선 라운드 반지름. + static let cornerRadius: CGFloat = 8.0 + /// 경계선 두께. + static let borderWidth: CGFloat = 1.0 + } + + // MARK: IBOutlets + + /// 값 그래프 배경 뷰. + @IBOutlet private weak var valueGraphBackgroundView: UIView! { + didSet { + valueGraphBackgroundView.layer.setBorder( + color: Asset.graphBorder.color, + width: Layer.borderWidth, + radius: Layer.cornerRadius + ) + } + } + /// 비율 그래프 배경 뷰. + @IBOutlet private weak var ratioGraphBackgroundView: UIView! { + didSet { + ratioGraphBackgroundView.layer.setBorder( + color: Asset.graphBorder.color, + width: Layer.borderWidth, + radius: Layer.cornerRadius + ) + } + } + + // MARK: View + + /// 값 그래프. + private var valueGraphView: ValueGraphView! { + didSet { + valueGraphView.dataSource = self + valueGraphView.delegate = self + } + } + /// 비율 그래프. + private var ratioGraphView: RatioGraphView! { + didSet { + ratioGraphView.dataSource = self + } + } + + // MARK: Property + + /// 7일간의 미세먼지 농도 값 모음. + var fineDustValues: [CGFloat] = [18, 67, 176, 135, 96, 79, 51] + /// 전체에 대한 마지막 값의 비율 + private var fineDustLastValueRatio: CGFloat { + let sum = fineDustValues.reduce(0, +) + let last = fineDustValues.last ?? 0.0 + return last / sum + } + /// 선택된 날짜. + private var selectedDate: Date = Date() + + // MARK: Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "미세먼지 분석".localized + createSubviews() + NotificationCenter.default.addObserver( + self, + selector: #selector(didFetchFineDustConcentration(_:)), + name: .fetchFineDustConcentrationDidSuccess, + object: nil + ) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // 값 새로 받아오고 서브뷰 초기화 + var array = [CGFloat]() + for _ in 0..<7 { + array.append(CGFloat.random(in: 10...200)) + } + fineDustValues = array + setConstraintsToSubviews() + initializeValueGraphView() + initializeRatioGraphView() + } + + // MARK: Method + + /// 미세먼지 농도 조회 통신이 완료된 노티피케이션을 받았을 경우 동작 정의. + @objc private func didFetchFineDustConcentration(_ notification: Notification) { + if let response = notification.userInfo?["data"] as? FineDustResponse { + print(response) + } + } +} + +// MARK: - ValueGraphView Data Source 구현 + +extension StatisticsViewController: ValueGraphViewDataSource { + var referenceDate: Date { + return selectedDate + } + var intakeAmounts: [CGFloat] { + return fineDustValues + } +} + +// MARK: - ValueGraphView Delegate 구현 + +extension StatisticsViewController: ValueGraphViewDelegate { + func valueGraphView( + _ valueGraphView: ValueGraphView, + didTapDoneButton button: UIBarButtonItem, + in datePicker: UIDatePicker + ) { + selectedDate = datePicker.date + } +} + +// MARK: - RatioGraphView Data Source 구현 + +extension StatisticsViewController: RatioGraphViewDataSource { + /// 흡입량 비율 + var intakeRatio: CGFloat { + return fineDustLastValueRatio + } +} + +// MARK: - Private Extension + +private extension StatisticsViewController { + /// 서브뷰 생성하여 프로퍼티에 할당. + func createSubviews() { + valueGraphView + = UIView.instantiate(fromXib: ValueGraphView.classNameToString) as? ValueGraphView + ratioGraphView + = UIView.instantiate(fromXib: RatioGraphView.classNameToString) as? RatioGraphView + valueGraphView.translatesAutoresizingMaskIntoConstraints = false + ratioGraphView.translatesAutoresizingMaskIntoConstraints = false + valueGraphBackgroundView.addSubview(valueGraphView) + ratioGraphBackgroundView.addSubview(ratioGraphView) + } + /// 서브뷰에 오토레이아웃 설정. + func setConstraintsToSubviews() { + NSLayoutConstraint.activate([ + valueGraphView.anchor.top.equal(to: valueGraphBackgroundView.anchor.top), + valueGraphView.anchor.leading.equal(to: valueGraphBackgroundView.anchor.leading), + valueGraphView.anchor.trailing.equal(to: valueGraphBackgroundView.anchor.trailing), + valueGraphView.anchor.bottom.equal(to: valueGraphBackgroundView.anchor.bottom), + ratioGraphView.anchor.top.equal(to: ratioGraphBackgroundView.anchor.top), + ratioGraphView.anchor.leading.equal(to: ratioGraphBackgroundView.anchor.leading), + ratioGraphView.anchor.trailing.equal(to: ratioGraphBackgroundView.anchor.trailing), + ratioGraphView.anchor.bottom.equal(to: ratioGraphBackgroundView.anchor.bottom) + ]) + } +} + +// MARK: - Value Graph Private Extension + +private extension StatisticsViewController { + /// 값 그래프 뷰 초기화. + func initializeValueGraphView() { + valueGraphView.setup() + } + /// 비율 그래프 뷰 초기화. + func initializeRatioGraphView() { + ratioGraphView.setup() + } +} diff --git a/FineDust/Statistics/Statistics.storyboard b/FineDust/Statistics/Statistics.storyboard new file mode 100644 index 00000000..84b11e39 --- /dev/null +++ b/FineDust/Statistics/Statistics.storyboard @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FineDust/Statistics/View/Ratio Graph/RatioGraphView.swift b/FineDust/Statistics/View/Ratio Graph/RatioGraphView.swift new file mode 100644 index 00000000..6c9d42d4 --- /dev/null +++ b/FineDust/Statistics/View/Ratio Graph/RatioGraphView.swift @@ -0,0 +1,161 @@ +// +// RatioGraphView.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +/// Ratio Graph View Data Source Protocol. +protocol RatioGraphViewDataSource: class { + /// 전체에 대한 부분의 비율. + var intakeRatio: CGFloat { get } +} + +/// 비율 그래프 뷰. +final class RatioGraphView: UIView { + + // MARK: Constant + + /// 상수 정리. + enum Constant { + /// 배경 뷰 높이와 전체 비율 섹션 뷰 높이의 차이. + static let entireSectionViewHeightDifference: CGFloat = 64.0 + /// 가운데 원형 뷰 반지름의 전체 비율 섹션 뷰 반지름과의 비율. + static let centerHoleViewRadiusRatio: CGFloat = 1.2 + } + + // MARK: Delegate + + /// Ratio Graph View Data Source. + weak var dataSource: RatioGraphViewDataSource? + + // MARK: Private Properties + + /// 전체에 대한 부분의 비율. + private var ratio: CGFloat { + return dataSource?.intakeRatio ?? 0.0 + } + /// 비율을 각도로 변환. + private var endAngle: CGFloat { + return ratio * 2 * .pi - .pi / 2 + } + /// 배경 뷰 높이. + private var backgroundViewHeight: CGFloat { + return backgroundView.bounds.height - Constant.entireSectionViewHeightDifference + } + + // MARK: IBOutlet + + /// 배경 뷰. + @IBOutlet private weak var backgroundView: UIView! + + // MARK: View + + /// 원 그래프의 전체 비율 부분 뷰. + private var entireSectionView: UIView! + /// 가운데 비어 있는 원. + private var centerHoleView: UIView! + /// 퍼센트 레이블. + private var percentLabel: UILabel! + + // MARK: Method + + /// 뷰 전체 설정. + func setup() { + if entireSectionView != nil { + deinitializeSubviews() + } + drawEntireSectionView() + drawPortionSectionView() + drawCenterHoleView() + setPercentLabel() + } +} + +// MARK: - View Drawing + +private extension RatioGraphView { + /// 서브뷰 초기화. + func deinitializeSubviews() { + entireSectionView.removeFromSuperview() + centerHoleView.removeFromSuperview() + percentLabel.removeFromSuperview() + } + /// 전체 비율 뷰 그리기. + func drawEntireSectionView() { + entireSectionView = UIView(frame: CGRect( + x: 0, + y: 0, + width: backgroundViewHeight, + height: backgroundViewHeight + )) + backgroundView.addSubview(entireSectionView) + entireSectionView.backgroundColor = Asset.graph1.color + entireSectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + entireSectionView.anchor.width.equal(toConstant: backgroundViewHeight), + entireSectionView.anchor.height.equal(toConstant: backgroundViewHeight), + entireSectionView.anchor.centerX.equal(to: backgroundView.anchor.centerX), + entireSectionView.anchor.centerY.equal(to: backgroundView.anchor.centerY) + ]) + entireSectionView.clipsToBounds = true + entireSectionView.layer.cornerRadius = backgroundViewHeight / 2 + } + /// 부분 비율 뷰 그리기. + func drawPortionSectionView() { + let path = UIBezierPath() + path.move(to: entireSectionView.center) + path.addLine(to: CGPoint( + x: entireSectionView.frame.width / 2, + y: entireSectionView.frame.height + )) + path.addArc( + withCenter: entireSectionView.center, + radius: backgroundViewHeight, + startAngle: -.pi / 2, + endAngle: endAngle, + clockwise: true + ) + path.close() + let shapeLayer = CAShapeLayer() + shapeLayer.path = path.cgPath + shapeLayer.fillColor = Asset.graphToday.color.cgColor + shapeLayer.applySketchShadow(color: .black, alpha: 0.5, x: 0, y: 0, blur: 8, spread: 0) + entireSectionView.layer.addSublayer(shapeLayer) + } + /// 가운데 빈 효과 내는 원 그리기. + func drawCenterHoleView() { + centerHoleView = UIView() + centerHoleView.backgroundColor = .white + backgroundView.addSubview(centerHoleView) + centerHoleView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + centerHoleView.anchor.width.equal( + toConstant: backgroundViewHeight / Constant.centerHoleViewRadiusRatio + ), + centerHoleView.anchor.height.equal( + toConstant: backgroundViewHeight / Constant.centerHoleViewRadiusRatio + ), + centerHoleView.anchor.centerX.equal(to: backgroundView.anchor.centerX), + centerHoleView.anchor.centerY.equal(to: backgroundView.anchor.centerY) + ]) + centerHoleView.clipsToBounds = true + centerHoleView.layer.cornerRadius + = backgroundViewHeight / 2 / Constant.centerHoleViewRadiusRatio + } + /// 비어 있는 원 안에 퍼센트 레이블 설정하기. + func setPercentLabel() { + percentLabel = UILabel() + percentLabel.font = UIFont.systemFont(ofSize: 25, weight: .bold) + percentLabel.text = "\(Int(ratio * 100))%" + percentLabel.translatesAutoresizingMaskIntoConstraints = false + centerHoleView.addSubview(percentLabel) + NSLayoutConstraint.activate([ + percentLabel.anchor.centerX.equal(to: centerHoleView.anchor.centerX), + percentLabel.anchor.centerY.equal(to: centerHoleView.anchor.centerY) + ]) + } +} diff --git a/FineDust/Statistics/View/Ratio Graph/RatioGraphView.xib b/FineDust/Statistics/View/Ratio Graph/RatioGraphView.xib new file mode 100644 index 00000000..65462890 --- /dev/null +++ b/FineDust/Statistics/View/Ratio Graph/RatioGraphView.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FineDust/Statistics/View/Value Graph/ValueGraphView.swift b/FineDust/Statistics/View/Value Graph/ValueGraphView.swift new file mode 100644 index 00000000..b70954a1 --- /dev/null +++ b/FineDust/Statistics/View/Value Graph/ValueGraphView.swift @@ -0,0 +1,241 @@ +// +// ValueGraphView.swift +// FineDust +// +// Created by Presto on 22/01/2019. +// Copyright © 2019 boostcamp3rd. All rights reserved. +// + +import UIKit + +/// Value Graph View Data Source. +protocol ValueGraphViewDataSource: class { + /// 기준 날짜. + var referenceDate: Date { get } + /// 기준 날짜로부터 7일간의 미세먼지 흡입량. + var intakeAmounts: [CGFloat] { get } +} + +/// Value Graph View Delegate. +protocol ValueGraphViewDelegate: class { + /// DatePicker의 Done 버튼을 눌렀을 때의 동작 정의. + func valueGraphView( + _ valueGraphView: ValueGraphView, + didTapDoneButton button: UIBarButtonItem, + in datePicker: UIDatePicker + ) +} + +/// 지정 날짜 기준 일주일 그래프 관련 뷰. +final class ValueGraphView: UIView { + + // MARK: Constant + + /// 레이어 관련 상수 모음. + enum Layer { + /// 경계선 두께. + static let borderWidth: CGFloat = 1.0 + } + /// 애니메이션 관련 상수 모음. + enum Animation { + /// 애니메이션 기간. + static let duration: TimeInterval = 0.3 + /// 애니메이션 지연. + static let delay: TimeInterval = 0.0 + /// 용수철 효과 정도. + static let damping: CGFloat = 0.7 + /// 용수철 효과 시작 속도. + static let springVelocity: CGFloat = 0.5 + /// 애니메이션 옵션. + static let option: UIView.AnimationOptions = [.curveEaseInOut] + } + + // MARK: Delegate + + /// Value Graph View Data Source. + weak var dataSource: ValueGraphViewDataSource? + /// Value Graph View Delegate. + weak var delegate: ValueGraphViewDelegate? + + // MARK: Property + + /// DatePicker 프로퍼티. + private lazy var datePicker: UIDatePicker = { + let picker = UIDatePicker() + picker.calendar = Calendar.current + picker.date = Date() + picker.datePickerMode = .date + picker.maximumDate = Date() + picker.minimumDate = Calendar.current.date(from: DateComponents(year: 2019, month: 1, day: 1)) + picker.locale = Locale(identifier: "ko_KR") + return picker + }() + /// DateFormatter 프로퍼티. + private lazy var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "yyyy년 M월 d일 EEEE" + return formatter + }() + + // MARK: Private Properties + + /// 선택된 날짜. + private var selectedDate: Date = Date() { + didSet { + dateTextField.text = dateFormatter.string(from: selectedDate) + } + } + /// 기준 날짜로부터 7일간의 미세먼지 흡입량. + private var intakeAmounts: [CGFloat] { + return dataSource?.intakeAmounts ?? [] + } + /// 미세먼지 흡입량의 최대값. + private var maxValue: CGFloat { + let max = intakeAmounts.max() ?? 0.0 + return max + 1.0 + } + /// 흡입량 모음을 최대값에 대한 비율로 산출. `1.0 - (비율)`. + private var intakeRatios: [CGFloat] { + return intakeAmounts.map { 1.0 - $0 / maxValue } + } + /// 주축 레이블. + private var axisTexts: [String] { + return ["\(Int(maxValue))", "\(Int(maxValue / 2))", "0"] + } + /// 일 텍스트. + private var dateTexts: [String] { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "ko_KR") + dateFormatter.dateFormat = "d" + var array = [Date].init(repeating: selectedDate, count: 7) + for (index, element) in array.enumerated() { + array[index] = element.before(days: index) + } + return array.map { dateFormatter.string(from: $0) }.reversed() + } + + // MARK: IBOutlets + + /// 날짜 표시 텍스트 필드. + @IBOutlet private weak var dateTextField: UITextField! { + didSet { + let toolBar = UIToolbar( + frame: CGRect( + x: 0, + y: 0, + width: UIScreen.main.bounds.width, + height: 44 + ) + ) + toolBar.items = [ + UIBarButtonItem( + barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil + ), + UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(doneButtonDidTap(_:)) + ) + ] + // 선택된 날짜 초기화 + selectedDate = Date() + dateTextField.inputView = datePicker + dateTextField.inputAccessoryView = toolBar + } + } + /// 제목 레이블. + @IBOutlet private weak var titleLabel: UILabel! + /// 요일 레이블 모음. + @IBOutlet private var dateLabels: [UILabel]! + /// 그래프 뷰 모음. + @IBOutlet private var graphViews: [UIView]! { + didSet { + for (index, view) in graphViews.enumerated() { + view.layer.setBorder( + radius: 2.0 + ) + view.backgroundColor = graphBackgroundColor(at: index) + } + } + } + /// 단위 레이블 모음. + @IBOutlet private var unitLabels: [UILabel]! + /// 그래프 높이 제약 모음. + @IBOutlet private var graphViewHeightConstraints: [NSLayoutConstraint]! + + // MARK: Methods + + override func awakeFromNib() { + super.awakeFromNib() + } + /// 뷰 전체 설정. + func setup() { + initializeHeights() + animateHeights() + setUnitLabels() + setDateLabelsTitle() + print(intakeAmounts, maxValue, intakeRatios) + } + /// 키보드에 달린 완료 버튼을 눌렀을 때의 동작 정의. + @objc private func doneButtonDidTap(_ sender: UIBarButtonItem) { + dateTextField.resignFirstResponder() + dateTextField.text = dateFormatter.string(from: datePicker.date) + selectedDate = datePicker.date + setup() + delegate?.valueGraphView(self, didTapDoneButton: sender, in: datePicker) + } +} + +// MARK: - Private Extension + +private extension ValueGraphView { + /// 그래프 뷰 높이 초기화. + func initializeHeights() { + for (index, constraint) in graphViewHeightConstraints.enumerated() { + graphViewHeightConstraints[index] = constraint.changedMultiplier(to: 1.0) + } + layoutIfNeeded() + } + /// 그래프 뷰 높이 제약에 애니메이션 효과 설정. + func animateHeights() { + for (index, ratio) in intakeRatios.enumerated() { + var heightConstraint = graphViewHeightConstraints[index] + DispatchQueue.main.asyncAfter(deadline: .now()) { [weak self] in + UIView.animate( + withDuration: Animation.duration, + delay: Animation.delay, + usingSpringWithDamping: Animation.damping, + initialSpringVelocity: Animation.springVelocity, + options: .curveEaseInOut, + animations: { + heightConstraint = heightConstraint.changedMultiplier(to: ratio) + self?.layoutIfNeeded() + }, + completion: nil + ) + } + } + } + /// 주축 레이블 설정. + func setUnitLabels() { + zip(unitLabels, axisTexts).forEach { (label, text) in + label.text = text + } + } + /// 요일 레이블 텍스트 설정. + func setDateLabelsTitle() { + zip(dateLabels, dateTexts).forEach { (label, text) in + label.text = text + } + } + /// 그래프 색상 구하기. + func graphBackgroundColor(at index: Int) -> UIColor? { + if index == 6 { + return Asset.graphToday.color + } + return index % 2 == 0 ? Asset.graph1.color : Asset.graph2.color + } +} diff --git a/FineDust/Statistics/View/Value Graph/ValueGraphView.xib b/FineDust/Statistics/View/Value Graph/ValueGraphView.xib new file mode 100644 index 00000000..2503af61 --- /dev/null +++ b/FineDust/Statistics/View/Value Graph/ValueGraphView.xib @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FineDust/Supporting Files/AppDelegate.swift b/FineDust/Supporting Files/AppDelegate.swift index 5cfc9cda..f9b2208c 100644 --- a/FineDust/Supporting Files/AppDelegate.swift +++ b/FineDust/Supporting Files/AppDelegate.swift @@ -6,88 +6,187 @@ // Copyright © 2019 boostcamp3rd. All rights reserved. // -import UIKit import CoreData +import CoreLocation +import UIKit + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - + var window: UIWindow? - - + + private lazy var locationManager: CLLocationManager = { + let manager = CLLocationManager() + manager.desiredAccuracy = kCLLocationAccuracyBest + manager.distanceFilter = kCLDistanceFilterNone + manager.delegate = self + return manager + }() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + window?.tintColor = Asset.graph1.color + UINavigationBar.appearance().tintColor = UIColor.white + UINavigationBar.appearance().barTintColor = Asset.graph1.color + UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white] + UITabBar.appearance().tintColor = .white + UITabBar.appearance().unselectedItemTintColor = UIColor.lightGray + UITabBar.appearance().barTintColor = Asset.graph1.color + UITextField.appearance().tintColor = .clear + locationManager.requestAlwaysAuthorization() + FineDustHK.shared.requestAuthorization() + toggleFirstExecutionFlag() + fetchAPI() return true } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - + + func applicationWillResignActive(_ application: UIApplication) { } + + func applicationDidEnterBackground(_ application: UIApplication) { } + func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + fetchAPI() } - + + func applicationDidBecomeActive(_ application: UIApplication) { } + func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - // Saves changes in the application's managed object context before the application terminates. self.saveContext() } - + // MARK: - Core Data stack - + lazy var persistentContainer: NSPersistentContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentContainer(name: "FineDust") - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - return container + let container = NSPersistentContainer(name: "FineDust") + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container }() - + // MARK: - Core Data Saving support - + func saveContext () { - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } + } } +} + +extension AppDelegate: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + print("위치 갱신됨") + let locale = Locale(identifier: "ko_KR") + guard let location = locations.last else { return } + let coordinate = location.coordinate + let convertedCoordinate = GeoConverter().convert( + sourceType: .WGS_84, + destinationType: .TM, + geoPoint: GeographicPoint(x: coordinate.longitude, y: coordinate.latitude) + ) + GeoInfo.shared.setLocation(x: convertedCoordinate?.x ?? 0, y: convertedCoordinate?.y ?? 0) + CLGeocoder().reverseGeocodeLocation(location, preferredLocale: locale) { placeMarks, error in + if let error = error { + print(error.localizedDescription) + return + } + // administrativeArea / country / locality / name + // 서울특별시 / 대한민국 / 강남구 / 강남대로 382 + } + manager.stopUpdatingLocation() + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("GPS Error: \(error.localizedDescription)") + } + + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + print("권한 허가 상태 변경: \(status)") + if status == .authorizedWhenInUse || status == .authorizedAlways { + locationManager.startUpdatingLocation() + } + } +} +// MARK: - API 응답 초기화 + +private extension AppDelegate { + /// API 호출하는 메소드 + func fetchAPI() { + DispatchQueue.global(qos: .background).async { [weak self] in + guard let `self` = self else { return } + self.fetchObservatory(self.fetchFineDustConcentration) + } + } + + /// 관측소 정보를 가져오는 메소드 + func fetchObservatory(_ completion: @escaping () -> Void) { + API.shared.fetchObservatory { response, error in + if let error = error { + NotificationCenter.default.post( + name: .fetchObservatoryDidError, + object: nil, + userInfo: ["error": error] + ) + return + } + guard let response = response else { return } + FineDustInfo.shared.set(observatory: response.observatory ?? "") + completion() + } + } + + /// 미세먼지 농도 정보를 가져오는 메소드 + func fetchFineDustConcentration() { + API.shared.fetchFineDustConcentration(term: .daily) { response, error in + if let error = error { + NotificationCenter.default.post( + name: .fetchFineDustConcentrationDidError, + object: nil, + userInfo: ["error": error] + ) + return + } + guard let response = response else { return } + FineDustInfo.shared.set(fineDustResponse: response) + } + } } +// MARK: - 첫 실행시에만 호출 + +extension AppDelegate { + /// 첫 실행시에만 날짜를 저장하도록 함 + func toggleFirstExecutionFlag() { + if !UserDefaults.standard.bool(forKey: "isFirstExecution") { + CoreDataManager.shared.save([User.installedDate: Date()], forType: User.self) { error in + if let error = error { + print(error.localizedDescription) + return + } + UserDefaults.standard.set(true, forKey: "isFirstExecution") + } + } + } +} diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json index d8db8d65..7928053b 100644 --- a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,88 +1,147 @@ { "images" : [ { - "idiom" : "iphone", "size" : "20x20", + "idiom" : "iphone", + "filename" : "notificationicon@2x.png", "scale" : "2x" }, { - "idiom" : "iphone", "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon-20@3x.png", "scale" : "3x" }, { + "size" : "29x29", "idiom" : "iphone", + "filename" : "icon-small.png", + "scale" : "1x" + }, + { "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-small@2x.png", "scale" : "2x" }, { - "idiom" : "iphone", "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-small@3x.png", "scale" : "3x" }, { - "idiom" : "iphone", "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@2x.png", "scale" : "2x" }, { - "idiom" : "iphone", "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@3x.png", "scale" : "3x" }, { + "size" : "57x57", "idiom" : "iphone", - "size" : "60x60", + "filename" : "icon.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "icon@2x.png", "scale" : "2x" }, { + "size" : "60x60", "idiom" : "iphone", + "filename" : "icon-60@2x.png", + "scale" : "2x" + }, + { "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@3x.png", "scale" : "3x" }, { - "idiom" : "ipad", "size" : "20x20", + "idiom" : "ipad", + "filename" : "notificationicon~ipad.png", "scale" : "1x" }, { - "idiom" : "ipad", "size" : "20x20", + "idiom" : "ipad", + "filename" : "notificationicon-ipad@2x.png", "scale" : "2x" }, { - "idiom" : "ipad", "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-small-ipad.png", "scale" : "1x" }, { - "idiom" : "ipad", "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-small-ipad@2x.png", "scale" : "2x" }, { + "size" : "40x40", "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40-ipad@2x.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "icon-small-50.png", "scale" : "1x" }, { + "size" : "50x50", "idiom" : "ipad", - "size" : "40x40", + "filename" : "icon-small-50@2x.png", "scale" : "2x" }, { + "size" : "72x72", "idiom" : "ipad", - "size" : "76x76", + "filename" : "icon-72.png", "scale" : "1x" }, { + "size" : "72x72", "idiom" : "ipad", - "size" : "76x76", + "filename" : "icon-72@2x.png", "scale" : "2x" }, { + "size" : "76x76", "idiom" : "ipad", + "filename" : "icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x" + }, + { "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon-83.5@2x.png", "scale" : "2x" }, { diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png new file mode 100644 index 00000000..a14216ca Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40-ipad@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40-ipad@2x.png new file mode 100644 index 00000000..fad5175a Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40-ipad@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 00000000..9b29cf5f Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 00000000..fad5175a Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 00000000..ff297fe4 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 00000000..ff297fe4 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 00000000..1c372fab Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-72.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-72.png new file mode 100644 index 00000000..873e7f16 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-72.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png new file mode 100644 index 00000000..d65bc1ac Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-76.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 00000000..2783b024 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 00000000..63c7affc Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 00000000..abeb59fc Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-50.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-50.png new file mode 100644 index 00000000..bf2b38cf Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-50.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png new file mode 100644 index 00000000..243f15b7 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-ipad.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-ipad.png new file mode 100644 index 00000000..afe00a4c Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-ipad.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-ipad@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-ipad@2x.png new file mode 100644 index 00000000..0e94e0c6 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small-ipad@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small.png new file mode 100644 index 00000000..afe00a4c Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png new file mode 100644 index 00000000..0e94e0c6 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png new file mode 100644 index 00000000..dd92f166 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 00000000..5c1e349b Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon@2x.png new file mode 100644 index 00000000..0b40c384 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon-ipad@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon-ipad@2x.png new file mode 100644 index 00000000..9b29cf5f Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon-ipad@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon@2x.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon@2x.png new file mode 100644 index 00000000..9b29cf5f Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon@2x.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon~ipad.png b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon~ipad.png new file mode 100644 index 00000000..3c697e57 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/AppIcon.appiconset/notificationicon~ipad.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/barChartTabIcon.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/barChartTabIcon.imageset/Contents.json new file mode 100644 index 00000000..78dd1a0f --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/barChartTabIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bar-chart-3.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/barChartTabIcon.imageset/bar-chart-3.png b/FineDust/Supporting Files/Assets.xcassets/barChartTabIcon.imageset/bar-chart-3.png new file mode 100644 index 00000000..0db5d717 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/barChartTabIcon.imageset/bar-chart-3.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/black45.colorset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/black45.colorset/Contents.json new file mode 100644 index 00000000..538af068 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/black45.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "45", + "alpha" : "1.000", + "blue" : "45", + "green" : "45" + } + } + } + ] +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/graph1.colorset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/graph1.colorset/Contents.json new file mode 100644 index 00000000..80d85438 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/graph1.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0x5F", + "alpha" : "1.000", + "blue" : "0xEE", + "green" : "0x6F" + } + } + } + ] +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/graph2.colorset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/graph2.colorset/Contents.json new file mode 100644 index 00000000..8d1c27a7 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/graph2.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0x64", + "alpha" : "1.000", + "blue" : "0xF8", + "green" : "0x9A" + } + } + } + ] +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/graphBorder.colorset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/graphBorder.colorset/Contents.json new file mode 100644 index 00000000..0844c7cd --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/graphBorder.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "239", + "alpha" : "1.000", + "blue" : "244", + "green" : "239" + } + } + } + ] +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/graphToday.colorset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/graphToday.colorset/Contents.json new file mode 100644 index 00000000..c80c10d7 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/graphToday.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0xFF", + "alpha" : "1.000", + "blue" : "0x61", + "green" : "0x55" + } + } + } + ] +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/grayDust.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/grayDust.imageset/Contents.json new file mode 100644 index 00000000..afed0036 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/grayDust.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "회색먼지.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git "a/FineDust/Supporting Files/Assets.xcassets/grayDust.imageset/\355\232\214\354\203\211\353\250\274\354\247\200.png" "b/FineDust/Supporting Files/Assets.xcassets/grayDust.imageset/\355\232\214\354\203\211\353\250\274\354\247\200.png" new file mode 100644 index 00000000..eafba7e4 Binary files /dev/null and "b/FineDust/Supporting Files/Assets.xcassets/grayDust.imageset/\355\232\214\354\203\211\353\250\274\354\247\200.png" differ diff --git a/FineDust/Supporting Files/Assets.xcassets/heart.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/heart.imageset/Contents.json new file mode 100644 index 00000000..ddb4cd21 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/heart.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "heart.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/heart.imageset/heart.png b/FineDust/Supporting Files/Assets.xcassets/heart.imageset/heart.png new file mode 100644 index 00000000..2333f4a1 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/heart.imageset/heart.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/info1.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/info1.imageset/Contents.json new file mode 100644 index 00000000..8b28b9fa --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/info1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "info1.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/info1.imageset/info1.jpg b/FineDust/Supporting Files/Assets.xcassets/info1.imageset/info1.jpg new file mode 100644 index 00000000..df7e4590 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/info1.imageset/info1.jpg differ diff --git a/FineDust/Supporting Files/Assets.xcassets/infoTabIcon.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/infoTabIcon.imageset/Contents.json new file mode 100644 index 00000000..3b84d762 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/infoTabIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "round-information-button-3.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/infoTabIcon.imageset/round-information-button-3.png b/FineDust/Supporting Files/Assets.xcassets/infoTabIcon.imageset/round-information-button-3.png new file mode 100644 index 00000000..2fac52ed Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/infoTabIcon.imageset/round-information-button-3.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/mainTabIcon.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/mainTabIcon.imageset/Contents.json new file mode 100644 index 00000000..c7f66847 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/mainTabIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "house-with-chimney.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/mainTabIcon.imageset/house-with-chimney.png b/FineDust/Supporting Files/Assets.xcassets/mainTabIcon.imageset/house-with-chimney.png new file mode 100644 index 00000000..a2209d82 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/mainTabIcon.imageset/house-with-chimney.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/redheart.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/redheart.imageset/Contents.json new file mode 100644 index 00000000..443df1ca --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/redheart.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "heart-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/redheart.imageset/heart-2.png b/FineDust/Supporting Files/Assets.xcassets/redheart.imageset/heart-2.png new file mode 100644 index 00000000..35e24c8c Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/redheart.imageset/heart-2.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/speechBubble1.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/speechBubble1.imageset/Contents.json new file mode 100644 index 00000000..1ee853aa --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/speechBubble1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "speechBubble1.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/speechBubble1.imageset/speechBubble1.png b/FineDust/Supporting Files/Assets.xcassets/speechBubble1.imageset/speechBubble1.png new file mode 100644 index 00000000..5fae7529 Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/speechBubble1.imageset/speechBubble1.png differ diff --git a/FineDust/Supporting Files/Assets.xcassets/speechBubble2.imageset/Contents.json b/FineDust/Supporting Files/Assets.xcassets/speechBubble2.imageset/Contents.json new file mode 100644 index 00000000..0497a5a7 --- /dev/null +++ b/FineDust/Supporting Files/Assets.xcassets/speechBubble2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "speechBubble2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FineDust/Supporting Files/Assets.xcassets/speechBubble2.imageset/speechBubble2.png b/FineDust/Supporting Files/Assets.xcassets/speechBubble2.imageset/speechBubble2.png new file mode 100644 index 00000000..5790365d Binary files /dev/null and b/FineDust/Supporting Files/Assets.xcassets/speechBubble2.imageset/speechBubble2.png differ diff --git a/FineDust/Supporting Files/GeoConverter.swift b/FineDust/Supporting Files/GeoConverter.swift new file mode 100644 index 00000000..ef7f0820 --- /dev/null +++ b/FineDust/Supporting Files/GeoConverter.swift @@ -0,0 +1,603 @@ +// +// GeoConverter.swift +// GeoConverter +// +// Created by SangwooLee on 2017. 7. 27.. +// Copyright © 2017년 sangwoo. All rights reserved. +// +import Foundation + +struct GeographicPoint { + let x: Double // longitude (경도) + let y: Double // latitude (위도) + let z: Double + + init(x: Double, y: Double, z: Double) { + self.x = x + self.y = y + self.z = z + } + + init(x: Double, y: Double) { + self.x = x + self.y = y + self.z = 0 + } + + init() { + self.x = 0 + self.y = 0 + self.z = 0 + } +} + +struct NisMapInfo { + let re: Double = 6371.00877 // 사용할 지구 반경 [km] + let grid: Double = 5.0 // 격자 간격 [km] + let slat1: Double = 30.0 // 표준 위도 [degree] + let slat2: Double = 60.0 // 표준 위도 [degree] + let olon: Double = 126.0 // 기준점의 경도 [degree] + let olat: Double = 38.0 // 기준점의 위도 [degree] + let xo: Double = 210 / 5.0 // 기준점의 X 좌표 [격자 거리] + let yo: Double = 675 / 5.0 // 기준점의 Y 좌표 [격자 거리] +} + +enum MapProjectionType { + case WGS_84 + case KATEC // TM128 + case TM + case GRS_80 + case UTMK + case GRID // for NIA Open API +} + +fileprivate enum DatumParam: Double { + case X = -146.43 + case Y = 507.89 + case Z = 681.46 +} + +fileprivate struct GeographicCoordinateData { + let mapProjectionType: MapProjectionType + let scaleFactor: Double + let longitudeCenter: Double + let latitudeCenter: Double + let falseNorthing: Double + let falseEasting: Double + let major: Double + let minor: Double + let es: Double + let esp: Double + let ind: Double + let sourceM: Double + let destinationM: Double + + init(mapProjectionType: MapProjectionType, scaleFactor: Double, longitudeCenter: Double, latitudeCenter: Double, + falseNorthing: Double, falseEasting: Double, major: Double, minor: Double) { + self.mapProjectionType = mapProjectionType + self.scaleFactor = scaleFactor + self.longitudeCenter = longitudeCenter + self.latitudeCenter = latitudeCenter + self.falseNorthing = falseNorthing + self.falseEasting = falseEasting + self.major = major + self.minor = minor + + let x = (minor / major) + es = 1.0 - x * x + esp = es / (1.0 - es) + ind = es < 0.00001 ? 1.0 : 0.0 + + sourceM = major * GeographicCoordinateData.mlfn( + e0: GeographicCoordinateData.e0fn(es), + e1: GeographicCoordinateData.e1fn(es), + e2: GeographicCoordinateData.e2fn(es), + e3: GeographicCoordinateData.e3fn(es), + phi: latitudeCenter) + destinationM = major * GeographicCoordinateData.mlfn( + e0: GeographicCoordinateData.e0fn(es), + e1: GeographicCoordinateData.e1fn(es), + e2: GeographicCoordinateData.e2fn(es), + e3: GeographicCoordinateData.e3fn(es), + phi: latitudeCenter) + } + + static func e0fn(_ x: Double) -> Double { + return 1.0 - 0.25 * x * (1.0 + x / 16.0 * (3.0 + 1.25 * x)) + } + + static func e1fn(_ x: Double) -> Double { + return 0.375 * x * (1.0 + 0.25 * x * (1.0 + 0.46875 * x)) + } + + static func e2fn(_ x: Double) -> Double { + return 0.05859375 * x * x * (1.0 + 0.75 * x) + } + + static func e3fn(_ x: Double) -> Double { + return x * x * x * (35.0 / 3072.0) + } + + static func mlfn(e0: Double, e1: Double, e2: Double, e3: Double, phi: Double) -> Double { + return e0 * phi - e1 * sin(2.0 * phi) + e2 * sin(4.0 * phi) - e3 * sin(6.0 * phi) + } + + static func asinz(_ value: Double) -> Double { + if abs(value) > 1 { + return asin(value > 0 ? 1 : -1) + } + + return asin(value) + } +} + +class GeoConverter { + enum converterError: Error { + case infinity + } + + fileprivate let geoCoordDatas: [MapProjectionType: GeographicCoordinateData] + fileprivate let espln: Double = 0.0000000001 + + init() { + geoCoordDatas = [ + .WGS_84: GeographicCoordinateData(mapProjectionType: .WGS_84, scaleFactor: 1, longitudeCenter: 0.0, latitudeCenter: 0.0, falseNorthing: 0.0, falseEasting: 0.0, major: 6378137.0, minor: 6356752.3142), + + .KATEC: GeographicCoordinateData(mapProjectionType: .KATEC, scaleFactor: 0.9999 /* 0.9996 */, longitudeCenter: 2.23402144255274 /* 2.22529479629277 */, latitudeCenter: 0.663225115757845, falseNorthing: 600000.0, falseEasting: 400000.0, major: 6377397.155, minor: 6356078.9633422494), + + .TM: GeographicCoordinateData(mapProjectionType: .TM, scaleFactor: 1, + // longitudeCenter: 2.21656815003280, // 127 + longitudeCenter: 2.21661859489671, // 127 + 10.485 minute + latitudeCenter: 0.663225115757845, falseNorthing: 500000.0, falseEasting: 200000.0, major: 6377397.155, minor: 6356078.9633422494), + + .GRS_80: GeographicCoordinateData(mapProjectionType: .GRS_80, scaleFactor: 1, longitudeCenter: 2.21656815003280, latitudeCenter: 0.663225115757845, falseNorthing: 500000.0, falseEasting: 200000.0, major: 6378137, minor: 6356752.3142), + + .UTMK: GeographicCoordinateData(mapProjectionType: .UTMK, scaleFactor: 0.9996, longitudeCenter: 2.22529479629277, latitudeCenter: 0.663225115757845, falseNorthing: 2000000.0, falseEasting: 1000000.0, major: 6378137, minor: 6356752.3141403558)] + } + + func convert(sourceType: MapProjectionType, destinationType: MapProjectionType, geoPoint: GeographicPoint) -> GeographicPoint? { + let sourcePoint = ({ () -> GeographicPoint? in + if sourceType == .WGS_84 { + return GeographicPoint(x: degreeToRadian(geoPoint.x), y: degreeToRadian(geoPoint.y)) + } else { + return tmToGeodetic(source: sourceType, inputPoint: geoPoint) + } + + })() + + guard sourcePoint != nil else { + return nil + } + + let destinationPoint = ({ () -> GeographicPoint? in + if destinationType == .WGS_84 { + return GeographicPoint(x: radianToDegree(sourcePoint!.x), y: radianToDegree(sourcePoint!.y)) + } else { + return geodeticToTm(destination: destinationType, inputPoint: sourcePoint!) + } + })() + + guard destinationPoint != nil else { + return nil + } + + return destinationPoint + } + + func getDistanceByWGS84(from: GeographicPoint, to: GeographicPoint) -> Double { + let fromLatitude = degreeToRadian(from.y) + let fromLongitude = degreeToRadian(from.x) + let toLatitude = degreeToRadian(to.y) + let toLongitude = degreeToRadian(to.x) + + let longitude = toLongitude - fromLongitude + let latitude = toLatitude - fromLatitude + + let a = pow(sin(latitude / 2), 2) + cos(fromLatitude) * cos(toLatitude) * pow(sin(longitude / 2), 2) + + return 6376.5 * 2 * atan2(sqrt(a), sqrt(1 - a)) + } + + // GeographicPoint.x : longitude, GeographicPoint.y : latitude + func wgs84ToGrid(_ point: GeographicPoint) -> GeographicPoint? { + let mapInfo = NisMapInfo() + let pi = asin(1.0) * 2.0 + let degRad = pi / 180.0 + let re = mapInfo.re / mapInfo.grid + let slat1 = mapInfo.slat1 * degRad + let slat2 = mapInfo.slat2 * degRad + let olon = mapInfo.olon * degRad + let olat = mapInfo.olat * degRad + var sn = tan(pi * 0.25 + slat2 * 0.5) / tan(pi * 0.25 + slat1 * 0.5) + sn = log(cos(slat1) / cos(slat2)) / log(sn) + var sf = tan(pi * 0.25 + slat1 * 0.5) + sf = pow(sf, sn) * cos(slat1) / sn + var ro = tan(pi * 0.25 + olat * 0.5) + ro = re * sf / pow(ro, sn) + + var ra = tan(pi * 0.25 + point.y * degRad * 0.5) + ra = re * sf / pow(ra, sn) + var theta = point.x * degRad - olon; + if theta > pi { + theta -= 2.0 * pi + } else if theta < -pi { + theta += 2.0 * pi + } + theta *= sn + + let wgs84Point = GeographicPoint(x: (ra * sin(theta)) + mapInfo.xo, y: (ro - ra * cos(theta)) + mapInfo.yo) + + return GeographicPoint(x: floor(wgs84Point.x + 1.5), y: floor(wgs84Point.y + 1.5)) + } + + private func getDistanceByKatec(from: GeographicPoint, to: GeographicPoint) -> Double? { + return getDistanceByFromType(fromType: .KATEC, from: from, to: to) + } + + private func getDistanceByTM(from: GeographicPoint, to: GeographicPoint) -> Double? { + return getDistanceByFromType(fromType: .TM, from: from, to: to) + } + + private func getDistanceByUTMK(from: GeographicPoint, to: GeographicPoint) -> Double? { + return getDistanceByFromType(fromType: .UTMK, from: from, to: to) + } + + private func getDistanceByGRS80(from: GeographicPoint, to: GeographicPoint) -> Double? { + return getDistanceByFromType(fromType: .GRS_80, from: from, to: to) + } + + + private func getDistanceByFromType(fromType: MapProjectionType, from: GeographicPoint, to: GeographicPoint) -> Double? { + let fromPoint = convert(sourceType: fromType, destinationType: .WGS_84, geoPoint: from) + let toPoint = convert(sourceType: fromType, destinationType: .WGS_84, geoPoint: to) + + guard fromPoint != nil && toPoint != nil else { + return nil + } + + return getDistanceByWGS84(from: fromPoint!, to: toPoint!) + } + + private func getTimeBySec(distance: Double) -> Int { + return Int(round(3600 * distance / 4)) + } + + private func getTimeByMin(distance: Double) -> Int { + return Int(ceil(Double(getTimeBySec(distance: distance)) / 60)) + } + + private func geodeticToTm(destination: MapProjectionType, inputPoint: GeographicPoint) -> GeographicPoint? { + let transformedPoint = transform(source: .WGS_84, destination: destination, geoPoint: inputPoint) + guard transformedPoint != nil else { + return nil + } + + let deltaLongitude = transformedPoint!.x - geoCoordDatas[destination]!.longitudeCenter + let sinForInputPointY = sin(transformedPoint!.y) + let cosForInputPointY = cos(transformedPoint!.y) + + if 0 != geoCoordDatas[destination]!.ind { + let b = cosForInputPointY * sin(deltaLongitude) + if abs(abs(b) - 1) < espln { + return nil // 무한대 에러 + } + } + + let al = cosForInputPointY * deltaLongitude + let als = al * al + let c = geoCoordDatas[destination]!.esp * cosForInputPointY * cosForInputPointY + let tq = tan(transformedPoint!.y) + let t = tq * tq + let con = 1 - geoCoordDatas[destination]!.es * sinForInputPointY * sinForInputPointY + let n = geoCoordDatas[destination]!.major / sqrt(con) + let ml = geoCoordDatas[destination]!.major * + GeographicCoordinateData.mlfn( + e0: GeographicCoordinateData.e0fn(geoCoordDatas[destination]!.es), + e1: GeographicCoordinateData.e1fn(geoCoordDatas[destination]!.es), + e2: GeographicCoordinateData.e2fn(geoCoordDatas[destination]!.es), + e3: GeographicCoordinateData.e3fn(geoCoordDatas[destination]!.es), + phi: transformedPoint!.y) + + let x = geoCoordDatas[destination]!.scaleFactor * n * al * (1 + als / 6 * (1 - t + c + als / 20 * (5 - 18 * t + t * t + 72 * c - 58 * geoCoordDatas[destination]!.esp))) + geoCoordDatas[destination]!.falseEasting + let y = geoCoordDatas[destination]!.scaleFactor * (ml - geoCoordDatas[destination]!.destinationM + n * tq * (als * (0.5 + als / 24 * (5 - t + 9 * c + 4 * c * c + als / 30 * (61 - 58 * t + t * t + 600 * c - 330 * geoCoordDatas[destination]!.esp))))) + geoCoordDatas[destination]!.falseNorthing + + return GeographicPoint(x: x, y: y) + } + + private func tmToGeodetic(source: MapProjectionType, inputPoint: GeographicPoint) -> GeographicPoint? { + let newPoint = GeographicPoint( + x: inputPoint.x - geoCoordDatas[source]!.falseEasting, + y: inputPoint.y - geoCoordDatas[source]!.falseNorthing) + let maxIter = 6 + let con = (geoCoordDatas[source]!.sourceM + newPoint.y / geoCoordDatas[source]!.scaleFactor) / geoCoordDatas[source]!.major + + func calculatePhi(value: Double, count: Int) -> Double? { + let deltaPhi = ((con + GeographicCoordinateData.e1fn(geoCoordDatas[source]!.es) * sin(2 * value) - GeographicCoordinateData.e2fn(geoCoordDatas[source]!.es) * sin(4 * value) + GeographicCoordinateData.e3fn(geoCoordDatas[source]!.es) * sin(6 * value)) / GeographicCoordinateData.e0fn(geoCoordDatas[source]!.es)) - value + let phi = value + deltaPhi + + if abs(deltaPhi) <= espln { + return phi + } else if count >= maxIter { + return nil // 무한대 에러 + } + + return calculatePhi(value: phi, count: count + 1) + } + + let phi = calculatePhi(value: con, count: 0) + if phi == nil { + return nil + } + + let pointForInd = ({ () -> GeographicPoint? in + if 0 != geoCoordDatas[source]!.ind { + let f = exp(inputPoint.x / geoCoordDatas[source]!.major * geoCoordDatas[source]!.scaleFactor) + let g = 0.5 * (f - 1 / f) + let temp = geoCoordDatas[source]!.latitudeCenter + inputPoint.y / (geoCoordDatas[source]!.major * geoCoordDatas[source]!.scaleFactor) + let h = cos(temp) + let con = sqrt((1 - h * h) / (1 + g * g)) + let y = temp < 0 ? -(GeographicCoordinateData.asinz(con)) : GeographicCoordinateData.asinz(con) + let x = 0 == g && 0 == h ? geoCoordDatas[source]!.longitudeCenter : + atan(g / h) + geoCoordDatas[source]!.longitudeCenter + + return GeographicPoint(x: x, y: y) + } else { + return nil + }})() + + let pointForPhi = ({ () -> GeographicPoint? in + if abs(phi!) < Double.pi / 2 { + let sinPhi = sin(phi!) + let cosPhi = cos(phi!) + let tanPhi = tan(phi!) + let c = geoCoordDatas[source]!.esp * cosPhi * cosPhi + let cs = c * c + let t = tanPhi * tanPhi + let ts = t * t + let cont = 1 - geoCoordDatas[source]!.es * sinPhi * sinPhi + let n = geoCoordDatas[source]!.major / sqrt(cont) + let r = n * (1 - geoCoordDatas[source]!.es) / cont + let d = newPoint.x / (n * geoCoordDatas[source]!.scaleFactor) + let ds = d * d + let x = geoCoordDatas[source]!.longitudeCenter + (d * (1 - ds / 6 * (1 + 2 * t + c - ds / 20 * (5 - 2 * c + 28 * t - 3 * cs + 8 * geoCoordDatas[source]!.esp + 24 * ts))) / cosPhi) + let partA = n * tanPhi * ds / r + let partB = 61 + 90 * t + 298 * c + 45 * ts - 252 * geoCoordDatas[source]!.esp - 3 * cs + let y = phi! - partA * (0.5 - ds / 24 * (5 + 3 * t + 10 * c - 4 * cs - 9 * geoCoordDatas[source]!.esp - ds / 30 * partB)) + + return GeographicPoint(x: x, y: y) + } else { + let x = geoCoordDatas[source]!.longitudeCenter + let y = Double.pi * 0.5 * sin(newPoint.y) + + return GeographicPoint(x: x, y: y) + }})() + + if let point = pointForInd { + return transform(source: source, destination: .WGS_84, geoPoint: point) + } else if let point = pointForPhi { + return transform(source: source, destination: .WGS_84, geoPoint: point) + } + + return nil + } + + private func geodeticToGeocentric(type: MapProjectionType, inputPoint: GeographicPoint) -> GeographicPoint? { + /* + * The function geodeticToGeocentric converts geodetic coordinates + * (latitude, longitude, and height) to geocentric coordinates (X, Y, Z), + * according to the current ellipsoid parameters. + * + * GeographicPoint + * X : Geodetic latitude in radians (input) + * Y : Geodetic longitude in radians (input) + * X : Geodetic height, in meters (input) + * + * GeographicPoint + * X : Calculated Geocentric X coordinate, in meters (output) + * Y : Calculated Geocentric Y coordinate, in meters (output) + * Z : Calculated Geocentric Z coordinate, in meters (output) + */ + + /* + ** Don't blow up if Latitude is just a little out of the value + ** range as it may just be a rounding issue. Also removed longitude + ** test, it should be wrapped by Math.cos() and Math.sin(). NFW for PROJ.4, Sep/2001. + */ + func getLatitude(x: Double) -> Double? { + if x < -helfPI && x > -1.001 * helfPI { + return -helfPI + } else if x > helfPI && x < 1.001 * helfPI { + return helfPI + } else if x < -helfPI || x > helfPI { + return nil // x out of range + } else { + return x + } + } + + let latitude = getLatitude(x: inputPoint.y) + if latitude == nil { + return nil + } + + let longitude = inputPoint.x > Double.pi ? inputPoint.x - Double.pi * 2 : inputPoint.x + let height = inputPoint.z + let sinLatitude = sin(latitude!) + let cosLatitude = cos(latitude!) + let rn = geoCoordDatas[type]!.major / sqrt(1.0e0 - geoCoordDatas[type]!.es * pow(sinLatitude, 2)) + + let outputPoint = GeographicPoint( + x: (rn + height) * cosLatitude * cos(longitude), + y: (rn + height) * cosLatitude * sin(longitude), + z: (rn * (1 - geoCoordDatas[type]!.es) + height) * sinLatitude) + + return outputPoint + } + + /** Convert_Geocentric_To_Geodetic + * The method used here is derived from 'An Improved Algorithm for + * Geocentric to Geodetic Coordinate Conversion', by Ralph Toms, Feb 1996 + */ + private func geocentricToGeodetic(type: MapProjectionType, inputPoint: GeographicPoint) -> GeographicPoint { + // square of distance from Z axis + let W2 = inputPoint.x * inputPoint.x + inputPoint.y * inputPoint.y + + // distance from Z axis + let W = sqrt(W2) + + // initial estimate of vertical component + let T0 = inputPoint.z * adC + + // initial estimate of horizontal component + let S0 = sqrt(T0 * T0 + W2) + + // Math.sin(B0), B0 is estimate of Bowring aux doubleiable + let sinB0 = T0 / S0 + + // Math.cos(B0) + let cosB0 = W / S0 + + // cube of Math.sin(B0) + let sin3B0 = sinB0 * sinB0 * sinB0 + + // corrected estimate of vertical component + let T1 = inputPoint.z + geoCoordDatas[type]!.minor * geoCoordDatas[type]!.esp * sin3B0 + + // numerator of Math.cos(phi1) + let sum = W - geoCoordDatas[type]!.major * geoCoordDatas[type]!.es * cosB0 * cosB0 * cosB0 + + // corrected estimate of horizontal component + let s1 = sqrt(T1 * T1 + sum * sum) + + // Math.sin(phi1), phi1 is estimated latitude + let sinP1 = T1 / s1 + + // Math.cos(phi1) + let cosP1 = sum / s1 + + // Earth radius at location + let rn = geoCoordDatas[type]!.major / sqrt(1.0 - geoCoordDatas[type]!.es * sinP1 * sinP1) + + // indicates location is in polar region + let atPole = inputPoint.x == 0 && inputPoint.y == 0 ? true : false + + let longitude = ({ () -> Double in + if inputPoint.x != 0 { + return atan2(inputPoint.y, inputPoint.x) + } else { + if inputPoint.y > 0 { + return helfPI + } else if inputPoint.y < 0 { + return -helfPI + } else { + return 0 + } + } + })() + + let latitude = ({ () -> Double in + if inputPoint.x == 0 && inputPoint.y == 0 && inputPoint.z == 0 { + return helfPI + } else if atPole == false { + return atan(sinP1 / cosP1) + } + + if inputPoint.x == 0 && inputPoint.y == 0 { + if inputPoint.z > 0 { + return helfPI + } else if inputPoint.z < 0 { + return -helfPI + } + } + + return 0 + })() + + let height = ({ () -> Double in + if inputPoint.x == 0 && inputPoint.y == 0 && inputPoint.z == 0 { + return -self.geoCoordDatas[type]!.minor + } + + if cosP1 >= self.cos67p5 { + return W / cosP1 - rn + } else if cosP1 <= -self.cos67p5 { + return W / -cosP1 - rn + } else { + return inputPoint.z / sinP1 + rn * (self.geoCoordDatas[type]!.es - 1) + } + })() + + let outputPoint = GeographicPoint( + x: longitude, + y: latitude, + z: height) + + return outputPoint + } + + private func geodeticToWGS84(_ geoPoint: GeographicPoint) -> GeographicPoint { + return GeographicPoint(x: geoPoint.x + DatumParam.X.rawValue, + y: geoPoint.y + DatumParam.Y.rawValue, + z: geoPoint.z + DatumParam.Z.rawValue) + } + + private func geodeticFromWGS84(_ geoPoint: GeographicPoint) -> GeographicPoint { + return GeographicPoint(x: geoPoint.x - DatumParam.X.rawValue, + y: geoPoint.y - DatumParam.Y.rawValue, + z: geoPoint.z - DatumParam.Z.rawValue) + } + + private func transform(source: MapProjectionType, destination: MapProjectionType, geoPoint: GeographicPoint) -> GeographicPoint? { + guard source != destination else { + return nil + } + + if (source == .KATEC || source == .TM) || (destination == .KATEC || destination == .TM) { + // Convert to geocentric coordinates. + if let point = geodeticToGeocentric(type: source, inputPoint: geoPoint) { + // Convert between datums + + let pointAddedWGS84 = ({ () -> GeographicPoint? in + if source == .KATEC || source == .TM { + return geodeticToWGS84(point) + } else { + return nil + } + })() + + let newPoint = ({ () -> GeographicPoint? in + if destination == .KATEC || destination == .TM { + return geodeticFromWGS84(pointAddedWGS84 == nil ? point : pointAddedWGS84!) + } else { + return nil + } + })() + + // Convert back to geodetic coordinates + if let point = newPoint { + return geocentricToGeodetic(type: destination, inputPoint: point) + } else if let point = pointAddedWGS84 { + return geocentricToGeodetic(type: destination, inputPoint: point) + } else { + return nil + } + } else { + return nil + } + } else { + return nil + } + } + + private func degreeToRadian(_ degree: Double) -> Double { + return degree * Double.pi / 180 + } + + private func radianToDegree(_ radian: Double) -> Double { + return radian * 180 / Double.pi + } + + private let helfPI = 0.5 * Double.pi + private let cos67p5 = 0.38268343236508977 // cosine of 67.5 degrees + private let adC = 1.0026000 +} diff --git a/FineDust/SwiftGen/Assets.swift b/FineDust/SwiftGen/Assets.swift new file mode 100644 index 00000000..013a10c1 --- /dev/null +++ b/FineDust/SwiftGen/Assets.swift @@ -0,0 +1,119 @@ +// swiftlint:disable all +// Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen + +#if os(OSX) + import AppKit.NSImage + internal typealias AssetColorTypeAlias = NSColor + internal typealias AssetImageTypeAlias = NSImage +#elseif os(iOS) || os(tvOS) || os(watchOS) + import UIKit.UIImage + internal typealias AssetColorTypeAlias = UIColor + internal typealias AssetImageTypeAlias = UIImage +#endif + +// swiftlint:disable superfluous_disable_command +// swiftlint:disable file_length + +// MARK: - Asset Catalogs + +// swiftlint:disable identifier_name line_length nesting type_body_length type_name +internal enum Asset { + internal static let barChartTabIcon = ImageAsset(name: "barChartTabIcon") + internal static let black45 = ColorAsset(name: "black45") + internal static let graph1 = ColorAsset(name: "graph1") + internal static let graph2 = ColorAsset(name: "graph2") + internal static let graphBorder = ColorAsset(name: "graphBorder") + internal static let graphToday = ColorAsset(name: "graphToday") + internal static let grayDust = ImageAsset(name: "grayDust") + internal static let heart = ImageAsset(name: "heart") + internal static let info1 = ImageAsset(name: "info1") + internal static let infoTabIcon = ImageAsset(name: "infoTabIcon") + internal static let mainTabIcon = ImageAsset(name: "mainTabIcon") + internal static let redheart = ImageAsset(name: "redheart") +} +// swiftlint:enable identifier_name line_length nesting type_body_length type_name + +// MARK: - Implementation Details + +internal struct ColorAsset { + internal fileprivate(set) var name: String + + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) + internal var color: AssetColorTypeAlias { + return AssetColorTypeAlias(asset: self) + } +} + +internal extension AssetColorTypeAlias { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) + convenience init!(asset: ColorAsset) { + let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(OSX) + self.init(named: NSColor.Name(asset.name), bundle: bundle) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +internal struct DataAsset { + internal fileprivate(set) var name: String + + #if os(iOS) || os(tvOS) || os(OSX) + @available(iOS 9.0, tvOS 9.0, OSX 10.11, *) + internal var data: NSDataAsset { + return NSDataAsset(asset: self) + } + #endif +} + +#if os(iOS) || os(tvOS) || os(OSX) +@available(iOS 9.0, tvOS 9.0, OSX 10.11, *) +internal extension NSDataAsset { + convenience init!(asset: DataAsset) { + let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) + self.init(name: asset.name, bundle: bundle) + #elseif os(OSX) + self.init(name: NSDataAsset.Name(asset.name), bundle: bundle) + #endif + } +} +#endif + +internal struct ImageAsset { + internal fileprivate(set) var name: String + + internal var image: AssetImageTypeAlias { + let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) + let image = AssetImageTypeAlias(named: name, in: bundle, compatibleWith: nil) + #elseif os(OSX) + let image = bundle.image(forResource: NSImage.Name(name)) + #elseif os(watchOS) + let image = AssetImageTypeAlias(named: name) + #endif + guard let result = image else { fatalError("Unable to load image named \(name).") } + return result + } +} + +internal extension AssetImageTypeAlias { + @available(iOS 1.0, tvOS 1.0, watchOS 1.0, *) + @available(OSX, deprecated, + message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") + convenience init!(asset: ImageAsset) { + #if os(iOS) || os(tvOS) + let bundle = Bundle(for: BundleToken.self) + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(OSX) + self.init(named: NSImage.Name(asset.name)) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +private final class BundleToken {} diff --git a/FineDust/SwiftGen/Storyboard.swift b/FineDust/SwiftGen/Storyboard.swift new file mode 100644 index 00000000..2f56e144 --- /dev/null +++ b/FineDust/SwiftGen/Storyboard.swift @@ -0,0 +1,80 @@ +// swiftlint:disable all +// Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen + +// swiftlint:disable sorted_imports +import Foundation +import UIKit + +// swiftlint:disable superfluous_disable_command +// swiftlint:disable file_length + +// MARK: - Storyboard Scenes + +// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name +internal enum StoryboardScene { + internal enum Common: StoryboardType { + internal static let storyboardName = "Common" + + internal static let initialScene = InitialSceneType(storyboard: Common.self) + } + internal enum Feedback: StoryboardType { + internal static let storyboardName = "Feedback" + + internal static let initialScene = InitialSceneType(storyboard: Feedback.self) + } + internal enum LaunchScreen: StoryboardType { + internal static let storyboardName = "LaunchScreen" + + internal static let initialScene = InitialSceneType(storyboard: LaunchScreen.self) + } + internal enum Main: StoryboardType { + internal static let storyboardName = "Main" + + internal static let initialScene = InitialSceneType(storyboard: Main.self) + } + internal enum Statistics: StoryboardType { + internal static let storyboardName = "Statistics" + + internal static let initialScene = InitialSceneType(storyboard: Statistics.self) + } +} +// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name + +// MARK: - Implementation Details + +internal protocol StoryboardType { + static var storyboardName: String { get } +} + +internal extension StoryboardType { + static var storyboard: UIStoryboard { + let name = self.storyboardName + return UIStoryboard(name: name, bundle: Bundle(for: BundleToken.self)) + } +} + +internal struct SceneType { + internal let storyboard: StoryboardType.Type + internal let identifier: String + + internal func instantiate() -> T { + let identifier = self.identifier + guard let controller = storyboard.storyboard.instantiateViewController(withIdentifier: identifier) as? T else { + fatalError("ViewController '\(identifier)' is not of the expected class \(T.self).") + } + return controller + } +} + +internal struct InitialSceneType { + internal let storyboard: StoryboardType.Type + + internal func instantiate() -> T { + guard let controller = storyboard.storyboard.instantiateInitialViewController() as? T else { + fatalError("ViewController is not of the expected class \(T.self).") + } + return controller + } +} + +private final class BundleToken {} diff --git a/README.md b/README.md index 30088c65..20dec899 100644 --- a/README.md +++ b/README.md @@ -1 +1,90 @@ -# BoostCamp iOS C-2 \ No newline at end of file +# Project FineDust + +![Language](https://img.shields.io/badge/swift-4.2-orange.svg) +![Platform](https://img.shields.io/badge/platform-ios-lightgrey.svg) + +부스트캠프 3기 iOS과정 C-2팀 + +## 팀원 정보 + +**[intmain](https://github.com/intmain)** + +[Jae-eun](https://github.com/Jae-eun) + +[zunzunzun](https://github.com/zunzunzun) + +[presto95](https://github.com/presto95) + +## 기획 + +### 본인이 마신 미세먼지량을 알려주는 미세먼지 정보 앱 + +- 사용자가 실외에서 걸었던 거리에 따라 마신 미세먼지량을 알려줍니다. +- 미세먼지 축적량을 날짜별로 비교 가능하게 그래프로 보여줍니다. +- 미세먼지 관련 정보를 제공합니다. + +## 디자인 + +### 현재까지 구현된 뷰 + +![1](./images/1.PNG) +![2](./images/2.PNG) +![3](./images/3.PNG) + +### 컬러칩 + +- ![#5f6fee](https://placehold.it/15/5f6fee/000000?text=+) #5F6FEE +- ![#649af8](https://placehold.it/15/649af8/000000?text=+) #649AF8 +- ![#ff5561](https://placehold.it/15/ff5561/000000?text=+) #FF5561 + +## 개발 + +### 활용 기술 + +- **HealthKit** 사용하여 사용자의 걸음 수 및 거리 가져오기 +- **국가대기오염정보 Open API** 사용하여 미세먼지 및 초미세먼지 정보 가져오기 +- **Core Location** 사용하여 현재 위치의 위도 및 경도, 주소 가져오기 +- **Core Data** 사용하여 미세먼지 축적량을 앱 내부에 보존하기 + +--- + +- **[SwiftLint](https://github.com/realm/SwiftLint)** 적용 + +```yaml +disabled_rules: +- leading_whitespace +- trailing_whitespace +- nesting + +excluded: +- FineDust/Supporting Files/AppDelegate.swift +- FineDust/Supporting Files/GeoConverter.swift + +line_length: + warning: 99 + error: 120 + +identifier_name: + excluded: + - x + - y +``` + +- StyleShare의 **[Swift Style Guide](https://github.com/StyleShare/swift-style-guide)** 준수 +- 스토리보드 및 에셋 사용을 용이하게 하기 위해 **[SwiftGen](https://github.com/SwiftGen/SwiftGen)** 사용 + +```yaml +xcassets: + inputs: FineDust/Supporting Files/Assets.xcassets + outputs: + templateName: swift4 + output: Assets.swift + +ib: + inputs: FineDust + outputs: + templateName: scenes-swift4 + output: Storyboard.swift +``` + +- `project.pbxproj` 파일의 충돌을 최소화하고 해결을 쉽게 하기 위해 **[xUnique](https://github.com/truebit/xUnique)** 사용 diff --git a/images/1.PNG b/images/1.PNG new file mode 100644 index 00000000..b9f10252 Binary files /dev/null and b/images/1.PNG differ diff --git a/images/2.PNG b/images/2.PNG new file mode 100644 index 00000000..44b3252b Binary files /dev/null and b/images/2.PNG differ diff --git a/images/3.PNG b/images/3.PNG new file mode 100644 index 00000000..cd670943 Binary files /dev/null and b/images/3.PNG differ diff --git a/swiftgen.yml b/swiftgen.yml new file mode 100644 index 00000000..76d4a8b3 --- /dev/null +++ b/swiftgen.yml @@ -0,0 +1,11 @@ +xcassets: + inputs: FineDust/Supporting Files/Assets.xcassets + outputs: + templateName: swift4 + output: Assets.swift + +ib: + inputs: FineDust + outputs: + templateName: scenes-swift4 + output: Storyboard.swift