├── Screenshot_1.png ├── Screenshot_2.png ├── Screenshot_3.png ├── SalesTraveling Localized.xls ├── sort-Xcode-project-file.sh ├── SalesTraveling ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── icon.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-Small@2x.png │ │ ├── Icon-Small@3x.png │ │ ├── Icon-Small-40@3x.png │ │ ├── Icon-Small-40@2x-1.png │ │ ├── Icon-Notification@2x.png │ │ ├── Icon-Notification@3x.png │ │ └── Contents.json ├── GPXs │ ├── Cardino Blue.gpx │ └── Taipei 101.gpx ├── Extensions │ ├── String+Localized.swift │ ├── NSObject+ClassName.swift │ ├── UserDefaults+Keys.swift │ ├── UIColor+VisualIdentity.swift │ ├── UIViewController+Alert.swift │ ├── UIAlertController+Init.swift │ ├── Array+Algorithm.swift │ └── CLLocationCoordinate2D+Codable.swift ├── zh-Hant.lproj │ └── Localizable.strings ├── en.lproj │ └── Localizable.strings ├── Models │ ├── HYCAnntation.swift │ ├── HYCPlacemark+PostalAddress.swift │ ├── TourModel.swift │ ├── DirectionModel.swift │ └── HYCPlacemark.swift ├── Managers │ ├── AlgorithmManager.swift │ ├── MapMananger.swift │ └── DataManager.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── Custom UI │ └── HYCLoadingView.swift ├── AppDelegate.swift └── Flows │ ├── Locate │ ├── AddressResultTableViewController.swift │ └── AddressResult.storyboard │ └── Map │ ├── MapViewModel.swift │ ├── Map.storyboard │ └── MapViewController.swift ├── SalesTraveling.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── SalesTraveling.xcscheme └── project.pbxproj ├── .gitignore ├── README.md ├── SalesTravelingTests ├── Info.plist └── DataManagerTests.swift ├── LICENSE.md ├── privacy_policy.md └── sort-Xcode-project-file /Screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/Screenshot_1.png -------------------------------------------------------------------------------- /Screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/Screenshot_2.png -------------------------------------------------------------------------------- /Screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/Screenshot_3.png -------------------------------------------------------------------------------- /SalesTraveling Localized.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling Localized.xls -------------------------------------------------------------------------------- /sort-Xcode-project-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | perl sort-Xcode-project-file SalesTraveling.xcodeproj/project.pbxproj 3 | -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollr76518/SalesTraveling/HEAD/SalesTraveling/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png -------------------------------------------------------------------------------- /SalesTraveling/GPXs/Cardino Blue.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cardino Blue 5 | 6 | 7 | -------------------------------------------------------------------------------- /SalesTraveling/GPXs/Taipei 101.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Taipei 101 5 | 6 | 7 | -------------------------------------------------------------------------------- /SalesTraveling.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SalesTraveling.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/String+Localized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/23. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension String { 12 | 13 | var localized: String { 14 | return NSLocalizedString(self, comment: "") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/NSObject+ClassName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extension.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/23. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSObject { 12 | 13 | class var ClassName: String { 14 | return String(describing: self) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/UserDefaults+Keys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Extension.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/26. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | 13 | enum Keys { 14 | static let FavoritePlacemarks = "FavoritePlacemarks" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/UIColor+VisualIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/25. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit.UIColor 10 | 11 | extension UIColor { 12 | 13 | static let brand = #colorLiteral(red: 0.7882352941, green: 0.2784313725, blue: 0.01960784314, alpha: 1) //(201, 71, 05) 14 | } 15 | -------------------------------------------------------------------------------- /SalesTraveling/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | SalesTraveling 4 | 5 | Created by Ryan on 2017/11/23. 6 | Copyright © 2017年 Hanyu. All rights reserved. 7 | */ 8 | 9 | "Search"="搜尋"; 10 | "Time"="時間"; 11 | "Distance"="距離"; 12 | "min"="分鐘"; 13 | "km"="公里"; 14 | "Source"="出發點"; 15 | "Prompt"="提示"; 16 | "Current location"="目前位置"; 17 | "There is no calculated tour can be saved."="目前尚無計算完成的旅途可儲存"; 18 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/UIViewController+Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Alert.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2019/6/19. 6 | // Copyright © 2019 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | func presentAlert(of message: String) { 14 | let alert = UIAlertController(title: "Prompt".localized, message: message) 15 | present(alert, animated: true, completion: nil) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SalesTraveling/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | SalesTraveling 4 | 5 | Created by Ryan on 2017/11/23. 6 | Copyright © 2017年 Hanyu. All rights reserved. 7 | */ 8 | 9 | "Search"="Search"; 10 | "Time"="Time"; 11 | "Distance"="Distance"; 12 | "min"="min"; 13 | "km"="km"; 14 | "Source"="Source"; 15 | "Prompt"="Prompt"; 16 | "Current location"="Current location"; 17 | "There is no calculated tour can be saved."="There is no calculated tour can be saved."; 18 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/UIAlertController+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAlertController+Extension.swift 3 | // showu_app_ios 4 | // 5 | // Created by Hanyu on 2018/3/19. 6 | // Copyright © 2018年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIAlertController { 12 | 13 | convenience init(title: String?, message: String?, handler: ((UIAlertAction) -> Swift.Void)? = nil) { 14 | self.init(title: title, message: message, preferredStyle: .alert) 15 | let action = UIAlertAction(title: "OK".localized, style: .default, handler: handler) 16 | self.addAction(action) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # CocoaPods 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Pods/ 31 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/Array+Algorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extension.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/11. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | 13 | func decompose() -> (Iterator.Element, [Iterator.Element])? { 14 | guard let first = first else { return nil } 15 | return (first, Array(self[1.. [(Element, Element)] { 19 | var tuples: [(Element, Element)] = [] 20 | 21 | for (index, object) in enumerated() where index != count - 1 { 22 | tuples.append((object, self[index + 1])) 23 | } 24 | 25 | return tuples 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SalesTraveling 2 | 3 | # ![Screenshot_1](Screenshot_1.png)![Screenshot_2](Screenshot_2.png)![Screenshot_3](Screenshot_3.png) 4 | 5 | 這是一個透過 Apple API 計算路徑的 App 服務。(https://developer.apple.com/documentation/mapkit/mkdirections) 6 | 7 | 試想從你的所在位置,要怎麼走才能最快繞完以下的地方 8 | 9 | -------- 10 | 11 | 台北 101 12 | 13 | 淡水老街 14 | 15 | 內湖美麗華 16 | 17 | 陽明山 18 | 19 | 基隆夜市 20 | 21 | -------- 22 | 23 | 24 | 25 | 透過窮舉法,簡單的計算各種路線的最短路線/最快路線。 26 | 27 | 28 | 29 | \- 適合旅遊之前的估算規劃 30 | 31 | \- 適合送貨之前的最短路線 32 | 33 | \- 適合超過5個以上目的地的路徑規劃 34 | 35 | 36 | 37 | 特別感謝: 38 | 39 | App icon: 周子棋 40 | 41 | iOS develop: 郭佳甯, 陳穎璿、喵仔閒聊群 42 | 43 | Feature: 徐仲威, 傅勻垣 44 | 45 | Bug: 姜惟傑, 陳知言 46 | 47 | 48 | 49 | 目前已在 App store 上架: 50 | 51 | https://apps.apple.com/tw/app/shodo/id1406118779 -------------------------------------------------------------------------------- /SalesTraveling/Models/HYCAnntation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HYCAnntation.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2020/2/7. 6 | // Copyright © 2020 Hanyu. All rights reserved. 7 | // 8 | 9 | import MapKit.MKAnnotation 10 | 11 | class HYCAnntation: NSObject { 12 | 13 | private let placemark: HYCPlacemark 14 | let sorted: Int 15 | 16 | init(placemark: HYCPlacemark, sorted: Int) { 17 | self.placemark = placemark 18 | self.sorted = sorted 19 | } 20 | } 21 | 22 | extension HYCAnntation: MKAnnotation { 23 | 24 | var coordinate: CLLocationCoordinate2D { 25 | return placemark.coordinate 26 | } 27 | 28 | var title: String? { 29 | return placemark.name 30 | } 31 | 32 | var subtitle: String? { 33 | return placemark.title 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SalesTravelingTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SalesTraveling/Models/HYCPlacemark+PostalAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HYCPlacemark+PostalAddress.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2019/6/19. 6 | // Copyright © 2019 Hanyu. All rights reserved. 7 | // 8 | 9 | import Contacts.CNPostalAddress 10 | 11 | extension HYCPlacemark { 12 | 13 | var toPostalAddress: CNPostalAddress { 14 | let postalAddress = CNMutablePostalAddress() 15 | postalAddress.street = street ?? "" 16 | postalAddress.city = city ?? "" 17 | postalAddress.state = state ?? "" 18 | postalAddress.postalCode = postalCode ?? "" 19 | postalAddress.country = country ?? "" 20 | postalAddress.isoCountryCode = isoCountryCode ?? "" 21 | postalAddress.subAdministrativeArea = subAdministrativeArea ?? "" 22 | postalAddress.subLocality = subLocality ?? "" 23 | return postalAddress.copy() as! CNPostalAddress 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /SalesTraveling/Managers/AlgorithmManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlgorithmManager.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/11. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AlgorithmManager { 12 | 13 | class func between(_ object: T, _ objects: [T]) -> [[T]] { 14 | guard let (head, tail) = objects.decompose() else { return [[object]] } 15 | return [[object] + objects] + between(object, tail).map { [head] + $0 } 16 | } 17 | 18 | class func permutations(_ objects: [T]) -> [[T]] { 19 | guard let (head, tail) = objects.decompose() else { return [[]] } 20 | return permutations(tail).flatMap { between(head, $0) } 21 | } 22 | 23 | class func factorial(_ number: Int) -> Int { 24 | if number == 1 { return 1 } 25 | return number * factorial(number - 1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SalesTraveling/Extensions/CLLocationCoordinate2D+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+Extension.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2018/7/18. 6 | // Copyright © 2018年 Hanyu. All rights reserved. 7 | // 8 | 9 | import CoreLocation.CLLocation 10 | 11 | extension CLLocationCoordinate2D: Codable { 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case latitude, longitude 15 | } 16 | 17 | public init(from decoder: Decoder) throws { 18 | let values = try decoder.container(keyedBy: CodingKeys.self) 19 | let latitude = try values.decode(Double.self, forKey: .latitude) 20 | let longitude = try values.decode(Double.self, forKey: .longitude) 21 | self.init(latitude: latitude, longitude: longitude) 22 | } 23 | 24 | public func encode(to encoder: Encoder) throws { 25 | var container = encoder.container(keyedBy: CodingKeys.self) 26 | try container.encode(latitude, forKey: .latitude) 27 | try container.encode(longitude, forKey: .longitude) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Han-Yu, Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SalesTraveling/Models/TourModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TourModel.swift 3 | // SalesTraveling 4 | // 5 | // Created by Hanyu on 2017/10/23. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import MapKit.MKPlacemark 10 | 11 | struct TourModel: Codable { 12 | 13 | var directions: [DirectionModel] = [] 14 | } 15 | 16 | extension TourModel { 17 | 18 | var destinations: [HYCPlacemark] { 19 | return directions.map{ $0.destination } 20 | } 21 | 22 | var polylines: [MKPolyline] { 23 | return directions.map{ $0.polyline } 24 | } 25 | } 26 | 27 | extension TourModel { 28 | 29 | var distances: CLLocationDistance { 30 | return directions.map{ $0.distance }.reduce(0, +) 31 | } 32 | 33 | var sumOfExpectedTravelTime: TimeInterval { 34 | return directions.map{ $0.expectedTravelTime }.reduce(0, +) 35 | } 36 | 37 | var routeInformation: String { 38 | let distance = String(format: "Distance".localized + ": %.2f " + "km".localized, distances/1000) 39 | let time = String(format: "Time".localized + ": %.2f " + "min".localized, sumOfExpectedTravelTime/60) 40 | 41 | return distance + ", " + time 42 | } 43 | } 44 | 45 | extension TourModel: Comparable { 46 | 47 | static func <(lhs: TourModel, rhs: TourModel) -> Bool { 48 | return lhs.distances < rhs.distances 49 | } 50 | 51 | static func ==(lhs: TourModel, rhs: TourModel) -> Bool { 52 | return lhs.distances == rhs.distances 53 | } 54 | } 55 | 56 | extension TourModel: Hashable { 57 | 58 | func hash(into hasher: inout Hasher) { 59 | polylines.forEach { (polyline) in 60 | let coordinate = polyline.coordinate 61 | hasher.combine("\(coordinate.latitude)" + "+" + "\(coordinate.longitude)") 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SalesTraveling/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SalesTraveling/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | zh_TW 7 | CFBundleDisplayName 8 | Shōto 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSLocationUsageDescription 26 | 27 | NSLocationWhenInUseUsageDescription 28 | 開啟定位可快速在地圖找到裝置的目前位置 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Map 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UIStatusBarStyle 38 | UIStatusBarStyleLightContent 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /SalesTraveling/Custom UI/HYCLoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HYCLoadingView.swift 3 | // IndicatorPractice 4 | // 5 | // Created by Ryan on 2018/5/12. 6 | // Copyright © 2018年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HYCLoadingView { 12 | 13 | static let shared = HYCLoadingView() 14 | 15 | private lazy var backgroundView = makeTranslucentView() 16 | private lazy var activityIndicatorView = makeActivityIndicatorView() 17 | } 18 | 19 | extension HYCLoadingView { 20 | 21 | func show() { 22 | guard let mainWindow = UIApplication.shared.windows.first else { return } 23 | mainWindow.insertSubview(backgroundView, at: mainWindow.subviews.count) 24 | NSLayoutConstraint.activate([ 25 | backgroundView.topAnchor.constraint(equalTo: mainWindow.topAnchor), 26 | backgroundView.bottomAnchor.constraint(equalTo: mainWindow.bottomAnchor), 27 | backgroundView.leadingAnchor.constraint(equalTo: mainWindow.leadingAnchor), 28 | backgroundView.trailingAnchor.constraint(equalTo: mainWindow.trailingAnchor) 29 | ]) 30 | } 31 | 32 | func startIndicatorAnimation() { 33 | activityIndicatorView.startAnimating() 34 | } 35 | 36 | func stopIndicatorAnimation() { 37 | activityIndicatorView.stopAnimating() 38 | } 39 | 40 | func dismiss() { 41 | backgroundView.removeFromSuperview() 42 | } 43 | } 44 | 45 | extension HYCLoadingView { 46 | 47 | private func makeTranslucentView() -> UIView { 48 | let view = UIView(frame: .zero) 49 | view.backgroundColor = .black 50 | view.alpha = 0.65 51 | view.translatesAutoresizingMaskIntoConstraints = false 52 | view.addSubview(activityIndicatorView) 53 | NSLayoutConstraint.activate([ 54 | activityIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 55 | activityIndicatorView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 56 | ]) 57 | return view 58 | } 59 | 60 | private func makeActivityIndicatorView() -> UIActivityIndicatorView { 61 | let view = UIActivityIndicatorView(style: .whiteLarge) 62 | view.startAnimating() 63 | view.translatesAutoresizingMaskIntoConstraints = false 64 | return view 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SalesTraveling/Models/DirectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectionModel.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/21. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | struct DirectionModel: Codable, Equatable { 12 | 13 | let source: HYCPlacemark 14 | let destination: HYCPlacemark 15 | let distance: CLLocationDistance 16 | let expectedTravelTime: TimeInterval 17 | let polylineData: Data 18 | var polyline: MKPolyline { 19 | let buf = UnsafeMutableBufferPointer.allocate(capacity: polylineData.count / MemoryLayout.size) 20 | let _ = polylineData.copyBytes(to: buf) 21 | return MKPolyline(points: buf.baseAddress!, count: buf.count) 22 | } 23 | 24 | var sourcePlacemark: MKPlacemark { 25 | get { 26 | let postalAddress = source.toPostalAddress 27 | return MKPlacemark(coordinate: source.coordinate, postalAddress: postalAddress) 28 | } 29 | } 30 | 31 | var destinationPlacemark: MKPlacemark { 32 | get { 33 | let postalAddress = destination.toPostalAddress 34 | return MKPlacemark(coordinate: destination.coordinate, postalAddress: postalAddress) 35 | } 36 | } 37 | 38 | init(source: MKPlacemark, destination: MKPlacemark, routes: [MKRoute]) { 39 | self.source = HYCPlacemark(mkPlacemark: source) 40 | self.destination = HYCPlacemark(mkPlacemark: destination) 41 | self.distance = routes.first!.distance 42 | self.expectedTravelTime = routes.first!.expectedTravelTime 43 | let polyline = routes.first!.polyline 44 | self.polylineData = Data(buffer: UnsafeBufferPointer(start: polyline.points(), count: polyline.pointCount)) 45 | } 46 | 47 | init(source: HYCPlacemark, destination: HYCPlacemark, routes: [MKRoute]) { 48 | self.source = source 49 | self.destination = destination 50 | self.distance = routes.first!.distance 51 | self.expectedTravelTime = routes.first!.expectedTravelTime 52 | let polyline = routes.first!.polyline 53 | self.polylineData = Data(buffer: UnsafeBufferPointer(start: polyline.points(), count: polyline.pointCount)) 54 | } 55 | 56 | } 57 | 58 | extension DirectionModel: CustomStringConvertible { 59 | var description: String { 60 | return """ 61 | source: \(source) 62 | destination: \(destination) 63 | """ 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SalesTraveling/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SalesTraveling 4 | // 5 | // Created by Hanyu on 2017/10/22. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | UINavigationBar.appearance().tintColor = .white 20 | UINavigationBar.appearance().barTintColor = .brand 21 | UIToolbar.appearance().tintColor = .brand 22 | UITextField.appearance().tintColor = .brand 23 | UIButton.appearance().tintColor = .brand 24 | 25 | return true 26 | } 27 | 28 | func applicationWillResignActive(_ application: UIApplication) { 29 | // 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. 30 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 31 | } 32 | 33 | func applicationDidEnterBackground(_ application: UIApplication) { 34 | // 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. 35 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 36 | } 37 | 38 | func applicationWillEnterForeground(_ application: UIApplication) { 39 | // 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. 40 | } 41 | 42 | func applicationDidBecomeActive(_ application: UIApplication) { 43 | // 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. 44 | } 45 | 46 | func applicationWillTerminate(_ application: UIApplication) { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /SalesTraveling/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-Notification@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-Notification@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-Small@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-Small@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small-40@2x-1.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-Small-40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "size" : "20x20", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "idiom" : "ipad", 58 | "size" : "20x20", 59 | "scale" : "2x" 60 | }, 61 | { 62 | "idiom" : "ipad", 63 | "size" : "29x29", 64 | "scale" : "1x" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "size" : "29x29", 69 | "scale" : "2x" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "size" : "40x40", 74 | "scale" : "1x" 75 | }, 76 | { 77 | "idiom" : "ipad", 78 | "size" : "40x40", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "idiom" : "ipad", 83 | "size" : "76x76", 84 | "scale" : "1x" 85 | }, 86 | { 87 | "idiom" : "ipad", 88 | "size" : "76x76", 89 | "scale" : "2x" 90 | }, 91 | { 92 | "idiom" : "ipad", 93 | "size" : "83.5x83.5", 94 | "scale" : "2x" 95 | }, 96 | { 97 | "size" : "1024x1024", 98 | "idiom" : "ios-marketing", 99 | "filename" : "icon.png", 100 | "scale" : "1x" 101 | } 102 | ], 103 | "info" : { 104 | "version" : 1, 105 | "author" : "xcode" 106 | } 107 | } -------------------------------------------------------------------------------- /SalesTraveling/Managers/MapMananger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapMananger.swift 3 | // SalesTraveling 4 | // 5 | // Created by Hanyu on 2017/10/22. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | class MapMananger { } 12 | 13 | extension MapMananger { 14 | 15 | class func fetchLocalSearch(with keywords: String, region: MKCoordinateRegion, completion: @escaping (_ result: Result) -> ()) { 16 | let request = MKLocalSearch.Request() 17 | request.naturalLanguageQuery = keywords 18 | request.region = region 19 | 20 | let search = MKLocalSearch(request: request) 21 | search.start { (response, error) in 22 | if let response = response { 23 | completion(.success(response)) 24 | } 25 | 26 | if let error = error { 27 | completion(.failure(error)) 28 | } 29 | } 30 | } 31 | } 32 | 33 | extension MapMananger { 34 | 35 | class func reverseCoordinate(_ coordinate: CLLocationCoordinate2D, completion: @escaping (_ result: Result<[MKPlacemark], Error>) -> ()) { 36 | let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) 37 | let geocoder = CLGeocoder() 38 | geocoder.reverseGeocodeLocation(location) { (clPlacemarks, error) in 39 | if let clPlacemarks = clPlacemarks { 40 | let placemarks = clPlacemarks.map { (clPlacemark) -> MKPlacemark in 41 | return MKPlacemark(placemark: clPlacemark) 42 | } 43 | completion(.success(placemarks)) 44 | } 45 | 46 | if let error = error { 47 | completion(.failure(error)) 48 | } 49 | } 50 | } 51 | } 52 | 53 | extension MapMananger { 54 | 55 | class func boundingMapRect(polylines: [MKPolyline]) -> MKMapRect { 56 | let westPoint = polylines.lazy.map{ $0.boundingMapRect.minX }.min() ?? 0 57 | let northPoint = polylines.lazy.map{ $0.boundingMapRect.minY }.min() ?? 0 58 | let eastPoint = polylines.lazy.map{ $0.boundingMapRect.maxX }.max() ?? 0 59 | let southPoint = polylines.lazy.map{ $0.boundingMapRect.maxY }.max() ?? 0 60 | 61 | let origin = MKMapPoint(x: westPoint, y: northPoint) 62 | let size = MKMapSize(width: eastPoint - westPoint, height: southPoint - northPoint) 63 | return MKMapRect(origin: origin, size: size) 64 | } 65 | } 66 | 67 | // MARK: - HYCPlacemark 68 | extension MapMananger { 69 | 70 | class func calculateDirections(from source: HYCPlacemark, to destination: HYCPlacemark, completion: @escaping (_ result: Result<[MKRoute], Error>) -> ()) { 71 | let request = MKDirections.Request() 72 | request.source = source.toMapItem 73 | request.destination = destination.toMapItem 74 | request.transportType = .automobile 75 | 76 | let directions = MKDirections(request: request) 77 | directions.calculate { (response, error) in 78 | if let response = response { 79 | completion(.success(response.routes)) 80 | } 81 | 82 | if let error = error { 83 | completion(.failure(error)) 84 | } 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /SalesTraveling/Flows/Locate/AddressResultTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressResultTableViewController.swift 3 | // SalesTraveling 4 | // 5 | // Created by Hanyu on 2017/10/22. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | 12 | protocol AddressResultTableViewControllerDataSource: AnyObject { 13 | 14 | func mapView(for vc: AddressResultTableViewController) -> MKMapView 15 | func favoritePlacemarks(for vc: AddressResultTableViewController) -> [HYCPlacemark] 16 | } 17 | 18 | protocol AddressResultTableViewControllerDelegate: AnyObject { 19 | 20 | func viewController(_ vc: AddressResultTableViewController, didSelectAt placemark: HYCPlacemark) 21 | func viewController(_ vc: AddressResultTableViewController, didRecevice error: Error) 22 | } 23 | 24 | class AddressResultTableViewController: UITableViewController { 25 | 26 | private var matchingPlacemarks = [HYCPlacemark]() 27 | private var mapView: MKMapView { 28 | guard let dataSource = dataSource else { 29 | fatalError("Must have dataSource") 30 | } 31 | return dataSource.mapView(for: self) 32 | } 33 | weak var dataSource: AddressResultTableViewControllerDataSource? 34 | weak var delegate: AddressResultTableViewControllerDelegate? 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | matchingPlacemarks = dataSource?.favoritePlacemarks(for: self) ?? [] 40 | } 41 | } 42 | 43 | // MARK: - UITableViewDataSource 44 | extension AddressResultTableViewController { 45 | 46 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return matchingPlacemarks.count 48 | } 49 | 50 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 52 | let placemark = matchingPlacemarks[indexPath.row] 53 | cell.textLabel?.text = placemark.name 54 | cell.detailTextLabel?.text = placemark.title 55 | return cell 56 | } 57 | } 58 | 59 | // MARK: - UITableViewDelegate 60 | extension AddressResultTableViewController { 61 | 62 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 63 | let placemark = matchingPlacemarks[indexPath.row] 64 | delegate?.viewController(self, didSelectAt: placemark) 65 | dismiss(animated: true, completion: nil) 66 | } 67 | } 68 | 69 | // MARK: - UISearchResultsUpdating 70 | extension AddressResultTableViewController: UISearchResultsUpdating { 71 | 72 | func updateSearchResults(for searchController: UISearchController) { 73 | //https://stackoverflow.com/questions/30790244/uisearchcontroller-show-results-even-when-search-bar-is-empty 74 | //為了讓 Favorites 顯示出來 75 | view.isHidden = false 76 | 77 | guard let keywords = searchController.searchBar.text else { return } 78 | 79 | MapMananger.fetchLocalSearch(with: keywords, region: mapView.region) { (status) in 80 | switch status { 81 | case .success(let response): 82 | self.matchingPlacemarks = response.mapItems.map{ HYCPlacemark(mkPlacemark: $0.placemark) } 83 | case .failure(let error): 84 | self.delegate?.viewController(self, didRecevice: error) 85 | self.matchingPlacemarks = self.dataSource?.favoritePlacemarks(for: self) ?? [] 86 | } 87 | self.tableView.reloadData() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /SalesTraveling.xcodeproj/xcshareddata/xcschemes/SalesTraveling.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 66 | 67 | 68 | 74 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /SalesTraveling/Models/HYCPlacemark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HYCPlacemark.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2019/6/15. 6 | // Copyright © 2019 Hanyu. All rights reserved. 7 | // 8 | 9 | import MapKit.MKPlacemark 10 | 11 | class HYCPlacemark: NSObject, Codable { 12 | 13 | var title: String? 14 | var subtitle: String? 15 | var coordinate: CLLocationCoordinate2D 16 | 17 | // PostalAddress properties 18 | var street: String? 19 | var city: String? 20 | var state: String? 21 | 22 | // address dictionary properties 23 | var name: String? 24 | var thoroughfare: String? 25 | var subThoroughfare: String? 26 | var locality: String? 27 | var subLocality: String? 28 | var administrativeArea: String? 29 | var subAdministrativeArea: String? 30 | var postalCode: String? 31 | var isoCountryCode: String? 32 | var country: String? 33 | var inlandWater: String? 34 | var ocean: String? 35 | var areasOfInterest: [String]? 36 | 37 | init(mkPlacemark: MKPlacemark) { 38 | name = mkPlacemark.name 39 | title = mkPlacemark.title 40 | if mkPlacemark.responds(to: #selector(getter: MKAnnotation.subtitle)) { 41 | subtitle = mkPlacemark.subtitle 42 | } 43 | coordinate = mkPlacemark.coordinate 44 | street = mkPlacemark.postalAddress?.street 45 | city = mkPlacemark.postalAddress?.city 46 | state = mkPlacemark.postalAddress?.state 47 | postalCode = mkPlacemark.postalAddress?.postalCode 48 | country = mkPlacemark.postalAddress?.country 49 | isoCountryCode = mkPlacemark.postalAddress?.isoCountryCode 50 | subAdministrativeArea = mkPlacemark.postalAddress?.subAdministrativeArea 51 | subLocality = mkPlacemark.postalAddress?.subLocality 52 | } 53 | 54 | internal init(title: String? = nil, subtitle: String? = nil, coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0), street: String? = nil, city: String? = nil, state: String? = nil, name: String? = nil, thoroughfare: String? = nil, subThoroughfare: String? = nil, locality: String? = nil, subLocality: String? = nil, administrativeArea: String? = nil, subAdministrativeArea: String? = nil, postalCode: String? = nil, isoCountryCode: String? = nil, country: String? = nil, inlandWater: String? = nil, ocean: String? = nil, areasOfInterest: [String]? = nil) { 55 | self.title = title 56 | self.subtitle = subtitle 57 | self.coordinate = coordinate 58 | self.street = street 59 | self.city = city 60 | self.state = state 61 | self.name = name 62 | self.thoroughfare = thoroughfare 63 | self.subThoroughfare = subThoroughfare 64 | self.locality = locality 65 | self.subLocality = subLocality 66 | self.administrativeArea = administrativeArea 67 | self.subAdministrativeArea = subAdministrativeArea 68 | self.postalCode = postalCode 69 | self.isoCountryCode = isoCountryCode 70 | self.country = country 71 | self.inlandWater = inlandWater 72 | self.ocean = ocean 73 | self.areasOfInterest = areasOfInterest 74 | } 75 | 76 | // NSObject 的 == 要 override 這個 method 77 | override func isEqual(_ object: Any?) -> Bool { 78 | if let other = object as? HYCPlacemark { 79 | return coordinate.latitude == other.coordinate.latitude && coordinate.longitude == other.coordinate.longitude 80 | } else { 81 | return false 82 | } 83 | } 84 | } 85 | 86 | extension HYCPlacemark { 87 | 88 | var toMKPlacemark: MKPlacemark { 89 | var addressDictionary: [String : Any] = [:] 90 | addressDictionary["name"] = name 91 | addressDictionary["thoroughfare"] = thoroughfare 92 | addressDictionary["locality"] = locality 93 | addressDictionary["subLocality"] = subLocality 94 | addressDictionary["administrativeArea"] = administrativeArea 95 | addressDictionary["subAdministrativeArea"] = subAdministrativeArea 96 | addressDictionary["postalCode"] = postalCode 97 | addressDictionary["isoCountryCode"] = isoCountryCode 98 | addressDictionary["country"] = country 99 | addressDictionary["inlandWater"] = inlandWater 100 | addressDictionary["ocean"] = ocean 101 | addressDictionary["areasOfInterest"] = areasOfInterest 102 | return MKPlacemark(coordinate: coordinate, addressDictionary: addressDictionary) 103 | } 104 | 105 | var toMapItem: MKMapItem { 106 | let item = MKMapItem(placemark: toMKPlacemark) 107 | item.name = name 108 | return item 109 | } 110 | } 111 | 112 | extension HYCPlacemark { 113 | 114 | override var description: String { 115 | 116 | return """ 117 | name: \(String(describing: name)) 118 | """ 119 | 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /SalesTraveling/Managers/DataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2017/11/26. 6 | // Copyright © 2017年 Hanyu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | class DataManager { 13 | 14 | static let shared = DataManager() 15 | 16 | typealias DirectionsFetcher = (HYCPlacemark, HYCPlacemark, @escaping (Result<[MKRoute], Error>) -> ()) -> Void 17 | 18 | private let directionsFetcher: DirectionsFetcher 19 | 20 | init(directionsFetcher: @escaping DirectionsFetcher = MapMananger.calculateDirections) { 21 | self.directionsFetcher = directionsFetcher 22 | } 23 | } 24 | 25 | // MARK: - Direction 26 | extension DataManager { 27 | 28 | func save(direction: DirectionModel) { 29 | do { 30 | let data = try JSONEncoder().encode(direction) 31 | let key = createKeyBy(source: direction.source, destination: direction.destination) 32 | UserDefaults.standard.set(data, forKey: key) 33 | } catch { 34 | print("Cant save direction with \(error)") 35 | } 36 | } 37 | 38 | func save(directions: [DirectionModel]) { 39 | for directionModel in directions { 40 | save(direction: directionModel) 41 | } 42 | } 43 | 44 | func findDirection(source: HYCPlacemark, destination: HYCPlacemark) -> DirectionModel? { 45 | let key = createKeyBy(source: source, destination: destination) 46 | guard let data = UserDefaults.standard.object(forKey: key) as? Data, 47 | let direction = try? JSONDecoder().decode(DirectionModel.self, from: data) else { return nil } 48 | return direction 49 | } 50 | } 51 | 52 | // MARK: - HYCPlacemark 53 | extension DataManager { 54 | 55 | private func createKeyBy(source: HYCPlacemark, destination: HYCPlacemark) -> String { 56 | return "\(source.coordinate.latitude),\(source.coordinate.longitude) - \(destination.coordinate.latitude),\(destination.coordinate.longitude)" 57 | } 58 | 59 | private typealias Journey = (source: HYCPlacemark, destination: HYCPlacemark) 60 | 61 | func fetchDirections(ofNew placemark: HYCPlacemark, toOld placemarks: [HYCPlacemark], current userPlacemark: HYCPlacemark?, completeBlock: @escaping (Result<[DirectionModel], Error>)->()) { 62 | var journeys = [Journey]() 63 | 64 | if let userPlacemark = userPlacemark { 65 | journeys.append((userPlacemark, placemark)) 66 | } 67 | 68 | for oldPlacemark in placemarks { 69 | journeys.append((oldPlacemark, placemark)) 70 | journeys.append((placemark, oldPlacemark)) 71 | } 72 | 73 | directions(for: journeys, completeBlock: { result in 74 | DispatchQueue.main.async { 75 | completeBlock(result) 76 | } 77 | }) 78 | } 79 | 80 | private func directions(for journeys: [Journey], acc: [DirectionModel] = [], completeBlock: @escaping (Result<[DirectionModel], Error>)->()) { 81 | guard let (source, destination) = journeys.first else { 82 | return completeBlock(.success(acc)) 83 | } 84 | 85 | directionsFetcher(source, destination) { result in 86 | switch result { 87 | case .failure(let error): 88 | completeBlock(.failure(error)) 89 | 90 | case .success(let routes): 91 | let direction = DirectionModel(source: source, destination: destination, routes: routes) 92 | self.directions(for: Array(journeys.dropFirst()), acc: acc + [direction], completeBlock: completeBlock) 93 | } 94 | } 95 | } 96 | } 97 | 98 | // MARK: - Favorite placemark 99 | extension DataManager { 100 | 101 | func addToFavorites(placemark: HYCPlacemark) throws { 102 | do { 103 | try addToFavorites(placemarks: [placemark]) 104 | } catch { 105 | throw error 106 | } 107 | } 108 | 109 | func addToFavorites(placemarks: [HYCPlacemark]) throws { 110 | var favorites = favoritePlacemarks() 111 | placemarks.forEach { (placemark) in 112 | favorites.insert(placemark) 113 | } 114 | do { 115 | let data = try JSONEncoder().encode(favorites) 116 | let key = UserDefaults.Keys.FavoritePlacemarks 117 | UserDefaults.standard.set(data, forKey: key) 118 | } catch { 119 | throw error 120 | } 121 | } 122 | 123 | func favoritePlacemarks() -> Set { 124 | let key = UserDefaults.Keys.FavoritePlacemarks 125 | guard 126 | let data = UserDefaults.standard.object(forKey: key) as? Data, 127 | let placemarks = try? JSONDecoder().decode(Set.self, from: data) 128 | else { 129 | return Set() 130 | } 131 | return placemarks 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /privacy_policy.md: -------------------------------------------------------------------------------- 1 | ## Privacy Policy 2 | 3 | Han-Yu, Chen built the Shōto app as a Free app. This SERVICE is provided by Han-Yu, Chen at no cost and is intended for use as is. 4 | 5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. 6 | 7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy. 8 | 9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Shōto unless otherwise defined in this Privacy Policy. 10 | 11 | **Information Collection and Use** 12 | 13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information, including but not limited to GPS, Location. The information that I request will be retained on your device and is not collected by me in any way. 14 | 15 | The app does use third party services that may collect information used to identify you. 16 | 17 | Link to privacy policy of third party service providers used by the app 18 | 19 | * [Google Play Services](https://www.google.com/policies/privacy/) 20 | 21 | **Log Data** 22 | 23 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. 24 | 25 | **Cookies** 26 | 27 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. 28 | 29 | This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service. 30 | 31 | **Service Providers** 32 | 33 | I may employ third-party companies and individuals due to the following reasons: 34 | 35 | * To facilitate our Service; 36 | * To provide the Service on our behalf; 37 | * To perform Service-related services; or 38 | * To assist us in analyzing how our Service is used. 39 | 40 | I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose. 41 | 42 | **Security** 43 | 44 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security. 45 | 46 | **Links to Other Sites** 47 | 48 | This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. 49 | 50 | **Children’s Privacy** 51 | 52 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13\. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions. 53 | 54 | **Changes to This Privacy Policy** 55 | 56 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page. 57 | 58 | **Contact Us** 59 | 60 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at rollr76518@gmail.com. 61 | 62 | This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.firebaseapp.com/) -------------------------------------------------------------------------------- /SalesTraveling/Flows/Locate/AddressResult.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /SalesTravelingTests/DataManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManagerTests.swift 3 | // SalesTravelingTests 4 | // 5 | // Created by Caio Zullo on 20/08/2020. 6 | // Copyright © 2020 Hanyu. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import MapKit 11 | @testable import SalesTraveling 12 | 13 | class DataManagerTests: XCTestCase { 14 | 15 | func test_fetchDirections_succeedsWhenAllRequestsSucceeds() { 16 | let newPlacemark = HYCPlacemark(name: "new placemark") 17 | 18 | let currentToNewPlacemarkDirection = DirectionModel(source: HYCPlacemark(name: "current placemark"), destination: newPlacemark, routes: [MKRoute()]) 19 | 20 | let oldToNewPlacemarkDirection1 = DirectionModel(source: HYCPlacemark(name: "old placemark 1"), destination: newPlacemark, routes: [MKRoute()]) 21 | 22 | let newToOldPlacemarkDirection1 = DirectionModel(source: oldToNewPlacemarkDirection1.destination, destination: oldToNewPlacemarkDirection1.source, routes: [MKRoute()]) 23 | 24 | let oldToNewPlacemarkDirection2 = DirectionModel(source: HYCPlacemark(name: "old placemark 2"), destination: newPlacemark, routes: [MKRoute()]) 25 | 26 | let newToOldPlacemarkDirection2 = DirectionModel(source: oldToNewPlacemarkDirection2.destination, destination: oldToNewPlacemarkDirection2.source, routes: [MKRoute()]) 27 | 28 | let sut = DataManager(directionsFetcher: { source, destination, completion in 29 | completion(.success([MKRoute()])) 30 | }) 31 | 32 | let exp = expectation(description: "Wait for fetch completion") 33 | 34 | sut.fetchDirections(ofNew: newPlacemark, toOld: [oldToNewPlacemarkDirection1.source, oldToNewPlacemarkDirection2.source], current: currentToNewPlacemarkDirection.source) { result in 35 | switch result { 36 | case let .success(directions): 37 | XCTAssertEqual(directions.count, 5) 38 | XCTAssertTrue(directions.contains(currentToNewPlacemarkDirection), "missing currentToNewPlacemarkDirection") 39 | XCTAssertTrue(directions.contains(oldToNewPlacemarkDirection1), "missing oldToNewPlacemarkDirection1") 40 | XCTAssertTrue(directions.contains(newToOldPlacemarkDirection1), "missing newToOldPlacemarkDirection1") 41 | XCTAssertTrue(directions.contains(oldToNewPlacemarkDirection2), "missing oldToNewPlacemarkDirection2") 42 | XCTAssertTrue(directions.contains(newToOldPlacemarkDirection2), "missing newToOldPlacemarkDirection2") 43 | 44 | case let .failure(error): 45 | XCTFail("Failed with error: \(error)") 46 | } 47 | 48 | exp.fulfill() 49 | } 50 | 51 | waitForExpectations(timeout: 0.1) 52 | } 53 | 54 | func test_fetchDirections_failsWhenOneRequestFails() { 55 | let sut = DataManager(directionsFetcher: { source, destination, completion in 56 | completion(.failure(NSError(domain: "any", code: 0))) 57 | }) 58 | 59 | let exp = expectation(description: "Wait for fetch completion") 60 | 61 | sut.fetchDirections(ofNew: HYCPlacemark(), toOld: [HYCPlacemark()], current: nil) { result in 62 | switch result { 63 | case .success: 64 | XCTFail("Should have failed, but succeeded") 65 | 66 | case .failure:break 67 | } 68 | 69 | exp.fulfill() 70 | } 71 | 72 | waitForExpectations(timeout: 0.1) 73 | } 74 | 75 | func test_fetchDirections_doesntDeadlockWhenFetcherDispatchesToTheMainQueue() { 76 | let sut = DataManager(directionsFetcher: { source, destination, completion in 77 | DispatchQueue.main.async { 78 | completion(.success([MKRoute()])) 79 | } 80 | }) 81 | 82 | let exp = expectation(description: "Wait for fetch completion") 83 | 84 | sut.fetchDirections(ofNew: HYCPlacemark(), toOld: [HYCPlacemark()], current: nil) { result in 85 | switch result { 86 | case .success: break 87 | 88 | 89 | case let .failure(error): 90 | XCTFail("Failed with error: \(error)") 91 | } 92 | 93 | exp.fulfill() 94 | } 95 | 96 | waitForExpectations(timeout: 0.1) 97 | } 98 | 99 | func test_fetchDirections_completesOnMainThread() { 100 | let sut = DataManager(directionsFetcher: { source, destination, completion in 101 | DispatchQueue.global().async { 102 | completion(.success([MKRoute()])) 103 | } 104 | }) 105 | 106 | let exp = expectation(description: "Wait for fetch completion") 107 | 108 | sut.fetchDirections(ofNew: HYCPlacemark(), toOld: [HYCPlacemark()], current: nil) { result in 109 | XCTAssertTrue(Thread.isMainThread) 110 | exp.fulfill() 111 | } 112 | 113 | waitForExpectations(timeout: 0.1) 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /sort-Xcode-project-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | # Copyright (C) 2007, 2008, 2009, 2010 Apple Inc. All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions 7 | # are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # 2. Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # 3. Neither the name of Apple Inc. ("Apple") nor the names of 15 | # its contributors may be used to endorse or promote products derived 16 | # from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | # Script to sort "children" and "files" sections in Xcode project.pbxproj files 30 | 31 | use strict; 32 | 33 | use File::Basename; 34 | use File::Spec; 35 | use File::Temp qw(tempfile); 36 | use Getopt::Long; 37 | 38 | sub sortChildrenByFileName($$); 39 | sub sortFilesByFileName($$); 40 | 41 | # Files (or products) without extensions 42 | my %isFile = map { $_ => 1 } qw( 43 | create_hash_table 44 | jsc 45 | minidom 46 | testapi 47 | testjsglue 48 | ); 49 | 50 | my $printWarnings = 1; 51 | my $showHelp; 52 | 53 | my $getOptionsResult = GetOptions( 54 | 'h|help' => \$showHelp, 55 | 'w|warnings!' => \$printWarnings, 56 | ); 57 | 58 | if (scalar(@ARGV) == 0 && !$showHelp) { 59 | print STDERR "ERROR: No Xcode project files (project.pbxproj) listed on command-line.\n"; 60 | undef $getOptionsResult; 61 | } 62 | 63 | if (!$getOptionsResult || $showHelp) { 64 | print STDERR <<__END__; 65 | Usage: @{[ basename($0) ]} [options] path/to/project.pbxproj [path/to/project.pbxproj ...] 66 | -h|--help show this help message 67 | -w|--[no-]warnings show or suppress warnings (default: show warnings) 68 | __END__ 69 | exit 1; 70 | } 71 | 72 | for my $projectFile (@ARGV) { 73 | if (basename($projectFile) =~ /\.xcodeproj$/) { 74 | $projectFile = File::Spec->catfile($projectFile, "project.pbxproj"); 75 | } 76 | 77 | if (basename($projectFile) ne "project.pbxproj") { 78 | print STDERR "WARNING: Not an Xcode project file: $projectFile\n" if $printWarnings; 79 | next; 80 | } 81 | 82 | # Grab the mainGroup for the project file 83 | my $mainGroup = ""; 84 | open(IN, "< $projectFile") || die "Could not open $projectFile: $!"; 85 | while (my $line = ) { 86 | $mainGroup = $2 if $line =~ m#^(\s*)mainGroup = ([0-9A-F]{24} /\* .+ \*/);$#; 87 | } 88 | close(IN); 89 | 90 | my ($OUT, $tempFileName) = tempfile( 91 | basename($projectFile) . "-XXXXXXXX", 92 | DIR => dirname($projectFile), 93 | UNLINK => 0, 94 | ); 95 | 96 | # Clean up temp file in case of die() 97 | $SIG{__DIE__} = sub { 98 | close(IN); 99 | close($OUT); 100 | unlink($tempFileName); 101 | }; 102 | 103 | my @lastTwo = (); 104 | open(IN, "< $projectFile") || die "Could not open $projectFile: $!"; 105 | while (my $line = ) { 106 | if ($line =~ /^(\s*)files = \(\s*$/) { 107 | print $OUT $line; 108 | my $endMarker = $1 . ");"; 109 | my @files; 110 | while (my $fileLine = ) { 111 | if ($fileLine =~ /^\Q$endMarker\E\s*$/) { 112 | $endMarker = $fileLine; 113 | last; 114 | } 115 | push @files, $fileLine; 116 | } 117 | print $OUT sort sortFilesByFileName @files; 118 | print $OUT $endMarker; 119 | } elsif ($line =~ /^(\s*)children = \(\s*$/) { 120 | print $OUT $line; 121 | my $endMarker = $1 . ");"; 122 | my @children; 123 | while (my $childLine = ) { 124 | if ($childLine =~ /^\Q$endMarker\E\s*$/) { 125 | $endMarker = $childLine; 126 | last; 127 | } 128 | push @children, $childLine; 129 | } 130 | if ($lastTwo[0] =~ m#^\s+\Q$mainGroup\E = \{$#) { 131 | # Don't sort mainGroup 132 | print $OUT @children; 133 | } else { 134 | print $OUT sort sortChildrenByFileName @children; 135 | } 136 | print $OUT $endMarker; 137 | } else { 138 | print $OUT $line; 139 | } 140 | 141 | push @lastTwo, $line; 142 | shift @lastTwo if scalar(@lastTwo) > 2; 143 | } 144 | close(IN); 145 | close($OUT); 146 | 147 | unlink($projectFile) || die "Could not delete $projectFile: $!"; 148 | rename($tempFileName, $projectFile) || die "Could not rename $tempFileName to $projectFile: $!"; 149 | } 150 | 151 | exit 0; 152 | 153 | sub sortChildrenByFileName($$) 154 | { 155 | my ($a, $b) = @_; 156 | my $aFileName = $1 if $a =~ /^\s*[A-Z0-9]{24} \/\* (.+) \*\/,$/; 157 | my $bFileName = $1 if $b =~ /^\s*[A-Z0-9]{24} \/\* (.+) \*\/,$/; 158 | my $aSuffix = $1 if $aFileName =~ m/\.([^.]+)$/; 159 | my $bSuffix = $1 if $bFileName =~ m/\.([^.]+)$/; 160 | if ((!$aSuffix && !$isFile{$aFileName} && $bSuffix) || ($aSuffix && !$bSuffix && !$isFile{$bFileName})) { 161 | return !$aSuffix ? -1 : 1; 162 | } 163 | return lc($aFileName) cmp lc($bFileName); 164 | } 165 | 166 | sub sortFilesByFileName($$) 167 | { 168 | my ($a, $b) = @_; 169 | my $aFileName = $1 if $a =~ /^\s*[A-Z0-9]{24} \/\* (.+) in /; 170 | my $bFileName = $1 if $b =~ /^\s*[A-Z0-9]{24} \/\* (.+) in /; 171 | return lc($aFileName) cmp lc($bFileName); 172 | } 173 | -------------------------------------------------------------------------------- /SalesTraveling/Flows/Map/MapViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapViewModel.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2019/6/19. 6 | // Copyright © 2019 Hanyu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit.MKPlacemark 11 | 12 | protocol MapViewModelDelegate: AnyObject { 13 | 14 | func viewModel(_ viewModel: MapViewModel, didUpdateUserPlacemark placemark: HYCPlacemark, from oldValue: HYCPlacemark?) 15 | func viewModel(_ viewModel: MapViewModel, reload placemarks: [HYCPlacemark]) 16 | func viewModel(_ viewModel: MapViewModel, isFetching: Bool) 17 | func viewModel(_ viewModel: MapViewModel, didUpdatePolylines polylines: [MKPolyline]) 18 | func viewModel(_ viewModel: MapViewModel, didRecevice error: Error) 19 | func viewModel(_ viewModel: MapViewModel, shouldShowTableView show: Bool) 20 | } 21 | 22 | class MapViewModel { 23 | 24 | enum ViewModelError: Error, LocalizedError { 25 | case tourModelIsNil 26 | 27 | var errorDescription: String? { 28 | switch self { 29 | case .tourModelIsNil: 30 | return "There is no calculated tour can be saved.".localized 31 | } 32 | } 33 | } 34 | 35 | enum PreferResult: Int { 36 | case distance = 0 37 | case time = 1 38 | } 39 | 40 | weak var delegate: MapViewModelDelegate? 41 | 42 | private var _placemarks: [HYCPlacemark] = [] 43 | 44 | private(set) var preferResult: PreferResult = .distance { 45 | didSet { 46 | tourModel = tourModel(preferResult: preferResult, in: tourModels) 47 | } 48 | } 49 | 50 | private(set) var tourModels: [TourModel] = [] { 51 | didSet { 52 | tourModel = tourModel(preferResult: preferResult, in: tourModels) 53 | } 54 | } 55 | 56 | private(set) var tourModel: TourModel? { 57 | didSet { 58 | guard let tourModel = tourModel else { return } 59 | delegate?.viewModel(self, didUpdatePolylines: tourModel.polylines) 60 | delegate?.viewModel(self, reload: tourModel.destinations) 61 | } 62 | } 63 | 64 | var result: String? { 65 | return tourModel?.routeInformation 66 | } 67 | 68 | var placemarks: [HYCPlacemark] { 69 | return tourModel?.destinations ?? [] 70 | } 71 | 72 | private(set) var shouldShowTableView: Bool = false { 73 | didSet { 74 | delegate?.viewModel(self, shouldShowTableView: shouldShowTableView) 75 | } 76 | } 77 | 78 | private var error: Error? { 79 | didSet { 80 | guard let error = error else { return } 81 | delegate?.viewModel(self, didRecevice: error) 82 | } 83 | } 84 | 85 | private var deviceLocation: CLLocation? { 86 | didSet { 87 | guard let deviceLocation = deviceLocation else { return } 88 | delegate?.viewModel(self, isFetching: true) 89 | MapMananger.reverseCoordinate(deviceLocation.coordinate) { (status) in 90 | self.delegate?.viewModel(self, isFetching: false) 91 | 92 | switch status { 93 | case .failure(let error): 94 | self.error = error 95 | case .success(let placemarks): 96 | guard let first = placemarks.first else { return } 97 | let placemark = HYCPlacemark(mkPlacemark: first) 98 | self.userPlacemark = placemark 99 | } 100 | } 101 | } 102 | } 103 | 104 | private(set) var userPlacemark: HYCPlacemark? { 105 | didSet { 106 | guard let placemark = userPlacemark else { return } 107 | delegate?.viewModel(self, didUpdateUserPlacemark: placemark, from: oldValue) 108 | guard placemark != oldValue else { return } 109 | if self._placemarks.count == 0 { 110 | if ProcessInfo.processInfo.environment["will_add_mock_placemarks"] == "true" { 111 | self.addMockPlacemarks() 112 | } 113 | } else { 114 | let tempPlacemarks = _placemarks 115 | _placemarks = [] 116 | add(placemarks: tempPlacemarks) { [weak self] (result) in 117 | guard let self = self else { return } 118 | switch result { 119 | case .failure(let error): 120 | self.error = error 121 | case .success: 122 | self.delegate?.viewModel(self, didUpdateUserPlacemark: placemark, from: oldValue) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | // MARK: - Placemark 131 | extension MapViewModel { 132 | 133 | func add(placemark: HYCPlacemark, completion: ((Result) -> Void)?) { 134 | delegate?.viewModel(self, isFetching: true) 135 | DataManager.shared.fetchDirections(ofNew: placemark, toOld: _placemarks, current: userPlacemark) { (status) in 136 | self.delegate?.viewModel(self, isFetching: false) 137 | 138 | switch status { 139 | case .failure(let error): 140 | completion?(.failure(error)) 141 | case .success(let directionModels): 142 | DataManager.shared.save(directions: directionModels) 143 | 144 | var placemarks = self._placemarks 145 | placemarks.append(placemark) 146 | self._placemarks = placemarks 147 | self.tourModels = self.showResultOfCaculate(startAt: self.userPlacemark, placemarks: placemarks) 148 | completion?(.success(Void())) 149 | } 150 | } 151 | } 152 | 153 | func deletePlacemark(at index: Int) { 154 | let placemark = placemarks[index] 155 | _placemarks.removeAll { (_placemark) -> Bool in 156 | return _placemark == placemark 157 | } 158 | tourModels = showResultOfCaculate(startAt: userPlacemark, placemarks: _placemarks) 159 | } 160 | 161 | func placemark(at coordinate: CLLocationCoordinate2D) -> HYCPlacemark? { 162 | return _placemarks.first { (placemark) -> Bool in 163 | return placemark.coordinate.latitude == coordinate.latitude && 164 | placemark.coordinate.longitude == coordinate.longitude 165 | } 166 | } 167 | } 168 | 169 | // MARK: - Favorites 170 | extension MapViewModel { 171 | 172 | func addToFavorite(_ placemark: HYCPlacemark) { 173 | do { 174 | try DataManager.shared.addToFavorites(placemark: placemark) 175 | } catch { 176 | self.error = error 177 | } 178 | } 179 | 180 | func favoritePlacemarks() -> [HYCPlacemark] { 181 | let userCoordinate = self.userPlacemark?.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0) 182 | let set = DataManager.shared.favoritePlacemarks() 183 | return set 184 | //用與目前使用者的距離來排序 185 | .sorted(by: { (lhs, rhs) -> Bool in 186 | func distance(source: CLLocationCoordinate2D, destination: CLLocationCoordinate2D) -> Double { 187 | return sqrt(pow((source.latitude - destination.latitude), 2) + pow((source.longitude - destination.longitude), 2)) 188 | } 189 | let distanceOflhs = distance(source: lhs.coordinate, destination: userCoordinate) 190 | let distanceOfrhs = distance(source: rhs.coordinate, destination: userCoordinate) 191 | return distanceOflhs > distanceOfrhs 192 | }) 193 | } 194 | } 195 | 196 | // MARK: - TableView 197 | extension MapViewModel { 198 | 199 | func showTableView(show: Bool) { 200 | shouldShowTableView = show 201 | } 202 | } 203 | 204 | // MARK: - Location 205 | extension MapViewModel { 206 | 207 | func update(device location: CLLocation) { 208 | deviceLocation = location 209 | } 210 | } 211 | 212 | // MARK: - PerferResult 213 | extension MapViewModel { 214 | 215 | func set(preferResult: PreferResult) { 216 | self.preferResult = preferResult 217 | } 218 | } 219 | 220 | // MARK: - Private method 221 | private extension MapViewModel { 222 | 223 | func showResultOfCaculate(startAt userPlacemark: HYCPlacemark?, placemarks: [HYCPlacemark]) -> [TourModel] { 224 | var tourModels: [TourModel] = [] 225 | 226 | //TODO: 改成比較清楚的處理流 227 | //只有一個點的時候不需要做排列組合的計算 228 | if placemarks.count == 1 { 229 | if let source = userPlacemark, let destination = placemarks.first { 230 | if let directions = DataManager.shared.findDirection(source: source, destination: destination) { 231 | var tourModel = TourModel() 232 | tourModel.directions.append(directions) 233 | tourModels.append(tourModel) 234 | } 235 | } 236 | } else { 237 | //[1, 2, 3] -> [[1, 2, 3], [1, 3, 2], [2, 3, 1], [2, 1, 3], [3, 1, 2], [3, 2, 1]] 238 | let permutations = AlgorithmManager.permutations(placemarks) 239 | 240 | //[[(1, 2), (2, 3)], [(1, 3), (3, 2)], [(2, 3), (3, 1)], [(2, 1), (1, 3)], [(3, 1), (1, 2)], [(3, 2), (2, 1)]] 241 | let tuplesCollection = permutations.map { (placemarks) -> [(HYCPlacemark, HYCPlacemark)] in 242 | return placemarks.toTuple() 243 | } 244 | 245 | for (index, tuples) in tuplesCollection.enumerated() { 246 | let tourModel = TourModel() 247 | tourModels.append(tourModel) 248 | 249 | for (nestedIndex, tuple) in tuples.enumerated() { 250 | //先弄起點 251 | if nestedIndex == 0, let userPlacemark = userPlacemark { 252 | let source = userPlacemark, destination = tuple.0 253 | if let directions = DataManager.shared.findDirection(source: source, destination: destination) { 254 | tourModels[index].directions.append(directions) 255 | } 256 | } 257 | //再弄中間點 258 | let source = tuple.0, destination = tuple.1 259 | if let direction = DataManager.shared.findDirection(source: source, destination: destination) { 260 | tourModels[index].directions.append(direction) 261 | } 262 | } 263 | } 264 | } 265 | 266 | return tourModels 267 | } 268 | 269 | func tourModel(preferResult: PreferResult, in tourModels: [TourModel]) -> TourModel? { 270 | switch preferResult { 271 | case .distance: 272 | return tourModels.sorted().first 273 | case .time: 274 | return tourModels.sorted(by: { (lhs, rhs) -> Bool in 275 | return lhs.sumOfExpectedTravelTime < rhs.sumOfExpectedTravelTime 276 | }).first 277 | } 278 | } 279 | } 280 | 281 | // MARK: - Mock placemarks 282 | extension MapViewModel { 283 | 284 | func addMockPlacemarks() { 285 | let a: HYCPlacemark = { 286 | let placemark = HYCPlacemark(mkPlacemark: MKPlacemark(coordinate: CLLocationCoordinate2DMake(25.0416801, 121.508074))) 287 | placemark.name = "西門町" 288 | placemark.title = "臺北市萬華區中華路一段" 289 | return placemark 290 | }() 291 | 292 | let b: HYCPlacemark = { 293 | let placemark = HYCPlacemark(mkPlacemark: MKPlacemark(coordinate: CLLocationCoordinate2DMake(25.0157677, 121.5555731))) 294 | placemark.name = "木柵動物園" 295 | placemark.title = "臺北市文山區新光路二段30號" 296 | return placemark 297 | }() 298 | 299 | let c: HYCPlacemark = { 300 | let placemark = HYCPlacemark(mkPlacemark: MKPlacemark(coordinate: CLLocationCoordinate2DMake(25.063985, 121.575923))) 301 | placemark.name = "內湖好市多" 302 | placemark.title = "114台北市內湖區舊宗路一段268號" 303 | return placemark 304 | }() 305 | 306 | add(placemarks: [a, b, c]) { [weak self] (result) in 307 | switch result { 308 | case .failure(let error): 309 | self?.error = error 310 | case .success: 311 | break 312 | } 313 | } 314 | } 315 | 316 | func add(placemarks: [HYCPlacemark], completion: ((Result) -> Void)?) { 317 | DispatchQueue.global().async { 318 | let queue = OperationQueue() 319 | queue.maxConcurrentOperationCount = 1 320 | 321 | placemarks.forEach({ (placemark) in 322 | let blockOperation = BlockOperation(block: { 323 | let semaphore = DispatchSemaphore(value: 0) 324 | DispatchQueue.main.async { 325 | self.add(placemark: placemark, completion: { (result) in 326 | switch result { 327 | case .failure(let error): 328 | completion?(.failure(error)) 329 | semaphore.signal() 330 | queue.cancelAllOperations() 331 | case .success: 332 | semaphore.signal() 333 | } 334 | }) 335 | } 336 | semaphore.wait() 337 | }) 338 | queue.addOperation(blockOperation) 339 | }) 340 | 341 | queue.waitUntilAllOperationsAreFinished() 342 | DispatchQueue.main.async { 343 | completion?(.success(Void())) 344 | } 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /SalesTraveling/Flows/Map/Map.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /SalesTraveling/Flows/Map/MapViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapViewController.swift 3 | // SalesTraveling 4 | // 5 | // Created by Ryan on 2019/1/7. 6 | // Copyright © 2019年 Hanyu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | 12 | class MapViewController: UIViewController { 13 | 14 | enum SectionType: Int, CaseIterable { 15 | case result = 0 16 | case source = 1 17 | case destination = 2 18 | } 19 | 20 | @IBOutlet weak var tableView: UITableView! 21 | @IBOutlet weak var mapView: MKMapView! { 22 | didSet { 23 | mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMarkerAnnotationView.ClassName) 24 | } 25 | } 26 | @IBOutlet weak var movableView: UIVisualEffectView! 27 | @IBOutlet weak var constriantOfMovableViewHeight: NSLayoutConstraint! 28 | @IBOutlet weak var movableViewTopToMapViewBottom: NSLayoutConstraint! 29 | @IBOutlet weak var barButtonItemDone: UIBarButtonItem! 30 | @IBOutlet weak var barButtonItemEdit: UIBarButtonItem! 31 | @IBOutlet weak var toolbar: UIToolbar! 32 | @IBOutlet weak var segmentedControl: UISegmentedControl! { 33 | didSet { 34 | segmentedControl.setTitle("Distance".localized, forSegmentAt: 0) 35 | segmentedControl.setTitle("Time".localized, forSegmentAt: 1) 36 | } 37 | } 38 | 39 | private var viewModel = MapViewModel() 40 | private lazy var addressResultTableViewController = makeAddressResultTableViewController() 41 | private lazy var searchController = makeSearchController() 42 | private let locationManager = CLLocationManager() 43 | private var shouldUpdateLocation = true 44 | 45 | private let heightOfUnit: CGFloat = 44.0 46 | private var switchOnConstantOfMovableView: CGFloat { 47 | return -(mapView.bounds.height - heightOfUnit) 48 | } 49 | 50 | private var switchOffConstantOfMovableView: CGFloat { 51 | return -heightOfUnit * 2 52 | } 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | let _ = searchController 57 | viewModel.delegate = self 58 | setupLocationManager() 59 | layoutLeftBarButtonItem() 60 | } 61 | 62 | override func viewWillAppear(_ animated: Bool) { 63 | super.viewWillAppear(animated) 64 | layoutMovableView() 65 | } 66 | 67 | //MARK: - IBActions 68 | @IBAction func tapGestureRecognizerDidPressed(_ sender: UITapGestureRecognizer) { 69 | viewModel.showTableView(show: !viewModel.shouldShowTableView) 70 | } 71 | 72 | @IBAction func panGestureRecognizerDidPressed(_ sender: UIPanGestureRecognizer) { 73 | let touchPoint = sender.location(in: mapView) 74 | switch sender.state { 75 | case .began: 76 | break 77 | case .changed: 78 | movableViewTopToMapViewBottom.constant = -(mapView.bounds.height - touchPoint.y) 79 | case .ended, .failed, .cancelled: 80 | magnetTableView() 81 | default: 82 | break 83 | } 84 | } 85 | 86 | @IBAction func leftBarButtonItemDidPressed(_ sender: Any) { 87 | tableView.setEditing(!tableView.isEditing, animated: true) 88 | perform(#selector(layoutLeftBarButtonItem), with: nil, afterDelay: 0.25) 89 | } 90 | 91 | @objc 92 | func layoutLeftBarButtonItem() { 93 | func frameOfSegmentedControl(frame: CGRect, superframe: CGRect) -> CGRect { 94 | var newframe = frame 95 | newframe.size.width = superframe.width/2 96 | return newframe 97 | } 98 | segmentedControl.frame = frameOfSegmentedControl(frame: segmentedControl.frame, superframe: toolbar.frame) 99 | let container = UIBarButtonItem(customView: segmentedControl) 100 | let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 101 | let userTrackingBarButtonItem = MKUserTrackingBarButtonItem(mapView: self.mapView) 102 | toolbar.setItems([leftBarButtonItem(), flexibleSpace, container, flexibleSpace, userTrackingBarButtonItem], animated: false) 103 | } 104 | 105 | private func leftBarButtonItem() -> UIBarButtonItem { 106 | return tableView.isEditing ? barButtonItemDone:barButtonItemEdit 107 | } 108 | 109 | @IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) { 110 | let index = sender.selectedSegmentIndex 111 | guard let preferResult = MapViewModel.PreferResult(rawValue: index) else { return } 112 | viewModel.set(preferResult: preferResult) 113 | } 114 | } 115 | 116 | // MARK: - Lazy var func 117 | private extension MapViewController { 118 | 119 | func makeAddressResultTableViewController() -> AddressResultTableViewController { 120 | guard let vc = UIStoryboard(name: "AddressResult", bundle: nil).instantiateViewController(withIdentifier: AddressResultTableViewController.ClassName) as? AddressResultTableViewController else { 121 | fatalError("AddressResultTableViewController doesn't exist") 122 | } 123 | vc.dataSource = self 124 | vc.delegate = self 125 | return vc 126 | } 127 | 128 | func makeSearchController() -> UISearchController { 129 | let searchController = UISearchController(searchResultsController: addressResultTableViewController) 130 | searchController.searchResultsUpdater = addressResultTableViewController 131 | searchController.delegate = self 132 | 133 | let searchBar = searchController.searchBar 134 | searchBar.sizeToFit() 135 | searchBar.placeholder = "Search".localized 136 | if #available(iOS 13.0, *) { 137 | searchBar.searchTextField.backgroundColor = .white 138 | } else { 139 | // Fallback on earlier versions 140 | } 141 | navigationItem.titleView = searchController.searchBar 142 | 143 | searchController.hidesNavigationBarDuringPresentation = false 144 | searchController.dimsBackgroundDuringPresentation = true 145 | definesPresentationContext = true 146 | return searchController 147 | } 148 | } 149 | 150 | //MARK: - AddressResultTableViewControllerDataSource 151 | extension MapViewController: AddressResultTableViewControllerDataSource { 152 | 153 | func mapView(for vc: AddressResultTableViewController) -> MKMapView { 154 | return mapView 155 | } 156 | 157 | func favoritePlacemarks(for vc: AddressResultTableViewController) -> [HYCPlacemark] { 158 | return viewModel.favoritePlacemarks() 159 | } 160 | } 161 | 162 | //MARK: - AddressResultTableViewControllerDelegate 163 | extension MapViewController: AddressResultTableViewControllerDelegate { 164 | 165 | func viewController(_ vc: AddressResultTableViewController, didSelectAt placemark: HYCPlacemark) { 166 | searchController.searchBar.text = nil 167 | searchController.searchBar.resignFirstResponder() 168 | 169 | viewModel.add(placemark: placemark, completion: nil) 170 | } 171 | 172 | func viewController(_ vc: AddressResultTableViewController, didRecevice error: Error) { 173 | print(error.localizedDescription) 174 | } 175 | } 176 | 177 | //MARK: - UISearchControllerDelegate 178 | extension MapViewController: UISearchControllerDelegate { 179 | 180 | } 181 | 182 | // MARK: - Private func 183 | fileprivate extension MapViewController { 184 | 185 | func layoutMovableView() { 186 | movableView.layer.cornerRadius = 22.0 187 | movableView.layer.masksToBounds = true 188 | constriantOfMovableViewHeight.constant = view.frame.height 189 | } 190 | 191 | func setupLocationManager() { 192 | locationManager.delegate = self 193 | locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers 194 | locationManager.requestWhenInUseAuthorization() 195 | locationManager.requestLocation() 196 | } 197 | 198 | func magnetTableView() { 199 | let buffer = self.toolbar.bounds.height //覺得 44.0 是一個不錯的數值(一個 cell 高) 200 | if viewModel.shouldShowTableView { 201 | let shouldHide = (movableViewTopToMapViewBottom.constant > (switchOnConstantOfMovableView + buffer)) 202 | viewModel.showTableView(show: !shouldHide) 203 | } else { 204 | let shouldShow = (movableViewTopToMapViewBottom.constant < (switchOffConstantOfMovableView - buffer)) 205 | viewModel.showTableView(show: shouldShow) 206 | } 207 | } 208 | } 209 | 210 | // MARK: - MKMapViewDelegate 211 | extension MapViewController: MKMapViewDelegate { 212 | 213 | func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { 214 | if shouldUpdateLocation, let deviceLocation = userLocation.location { 215 | shouldUpdateLocation = false 216 | viewModel.update(device: deviceLocation) 217 | } 218 | } 219 | 220 | func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { 221 | guard let annotation = annotation as? HYCAnntation else { return nil } 222 | let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: MKMarkerAnnotationView.ClassName, for: annotation) as? MKMarkerAnnotationView 223 | annotationView?.canShowCallout = true 224 | annotationView?.leftCalloutAccessoryView = UIButton(type: .contactAdd) 225 | annotationView?.rightCalloutAccessoryView = UIButton(type: .infoLight) 226 | annotationView?.titleVisibility = .adaptive 227 | annotationView?.markerTintColor = .brand 228 | annotationView?.glyphTintColor = .white 229 | annotationView?.displayPriority = .required 230 | annotationView?.glyphText = "\(annotation.sorted)" 231 | return annotationView 232 | } 233 | 234 | func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { 235 | let renderer = MKPolylineRenderer(overlay: overlay) 236 | renderer.strokeColor = UIColor.brand.withAlphaComponent(0.65) 237 | renderer.lineWidth = 4.0 238 | return renderer 239 | } 240 | 241 | func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { 242 | guard 243 | let annotation = view.annotation, 244 | let placemark = viewModel.placemark(at: annotation.coordinate) 245 | else { 246 | print("view.annotation is nil") 247 | return 248 | } 249 | switch control { 250 | case let left where left == view.leftCalloutAccessoryView: 251 | viewModel.addToFavorite(placemark) 252 | // TODO: 提示使用者已加到搜尋紀錄,以便快速搜尋。 253 | case let right where right == view.rightCalloutAccessoryView: 254 | let mapItems = [placemark.toMapItem] 255 | let options = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving] 256 | MKMapItem.openMaps(with: mapItems, launchOptions: options) 257 | default: 258 | break 259 | } 260 | } 261 | } 262 | 263 | // MARK: - UITableViewDataSource 264 | extension MapViewController: UITableViewDataSource { 265 | 266 | func numberOfSections(in tableView: UITableView) -> Int { 267 | return SectionType.allCases.count 268 | } 269 | 270 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 271 | guard let type = SectionType(rawValue: section) else { 272 | return 0 273 | } 274 | switch type { 275 | case .result: 276 | return (viewModel.tourModel != nil) ? 1 : 0 277 | case .source: 278 | return (viewModel.userPlacemark != nil) ? 1 : 0 279 | case .destination: 280 | return viewModel.placemarks.count 281 | } 282 | } 283 | 284 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 285 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 286 | 287 | guard let type = SectionType(rawValue: indexPath.section) else { 288 | return cell 289 | } 290 | switch type { 291 | case .result: 292 | let cell2 = UITableViewCell(style: .default, reuseIdentifier: nil) 293 | cell2.textLabel?.text = viewModel.result 294 | return cell2 295 | case .source: 296 | let placemark = viewModel.userPlacemark 297 | cell.textLabel?.text = "Source".localized + ": " + "Current location".localized 298 | cell.detailTextLabel?.text = placemark?.title 299 | case .destination: 300 | let placemark = viewModel.placemarks[indexPath.row] 301 | cell.textLabel?.text = "\(indexPath.row + 1). " + (placemark.name ?? "") 302 | cell.detailTextLabel?.text = placemark.title 303 | } 304 | return cell 305 | } 306 | } 307 | 308 | // MARK: UITableViewDelegate 309 | extension MapViewController: UITableViewDelegate { 310 | 311 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 312 | tableView.deselectRow(at: indexPath, animated: true) 313 | viewModel.showTableView(show: false) 314 | 315 | guard let type = SectionType(rawValue: indexPath.section) else { 316 | return 317 | } 318 | switch type { 319 | case .result: 320 | break 321 | case .source: 322 | let placemark = viewModel.userPlacemark 323 | for annotation in mapView.annotations { 324 | if annotation.coordinate.latitude == placemark?.coordinate.latitude && 325 | annotation.coordinate.longitude == placemark?.coordinate.longitude { 326 | mapView.selectAnnotation(annotation, animated: true) 327 | } 328 | } 329 | case .destination: 330 | let placemark = viewModel.placemarks[indexPath.row] 331 | for annotation in mapView.annotations { 332 | if annotation.coordinate.latitude == placemark.coordinate.latitude && 333 | annotation.coordinate.longitude == placemark.coordinate.longitude { 334 | mapView.selectAnnotation(annotation, animated: true) 335 | } 336 | } 337 | } 338 | } 339 | 340 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 341 | guard let type = SectionType(rawValue: indexPath.section) else { 342 | return false 343 | } 344 | switch type { 345 | case .result: 346 | return false 347 | case .source: 348 | return false 349 | case .destination: 350 | return true 351 | } 352 | } 353 | 354 | func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { 355 | return .delete 356 | } 357 | 358 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 359 | if editingStyle == .delete { 360 | viewModel.deletePlacemark(at: indexPath.row) 361 | } 362 | } 363 | } 364 | 365 | // MARK: - UIGestureRecognizerDelegate 366 | extension MapViewController: UIGestureRecognizerDelegate { 367 | 368 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 369 | return !tableView.frame.contains(touch.location(in: movableView)) 370 | } 371 | } 372 | 373 | // MARK: - UIScrollViewDelegate 374 | extension MapViewController: UIScrollViewDelegate { 375 | 376 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 377 | if (scrollView.contentOffset.y < 0) || (scrollView.contentSize.height <= scrollView.frame.size.height) { 378 | movableViewTopToMapViewBottom.constant -= scrollView.contentOffset.y 379 | scrollView.contentOffset = CGPoint.zero 380 | } 381 | } 382 | 383 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 384 | magnetTableView() 385 | } 386 | } 387 | 388 | // MARK: - MapViewModelDelegate 389 | extension MapViewController: MapViewModelDelegate { 390 | 391 | func viewModel(_ viewModel: MapViewModel, didUpdateUserPlacemark placemark: HYCPlacemark, from oldValue: HYCPlacemark?) { 392 | guard oldValue != placemark else { 393 | return 394 | } 395 | tableView.reloadSections([SectionType.source.rawValue], with: .automatic) 396 | } 397 | 398 | func viewModel(_ viewModel: MapViewModel, reload placemarks: [HYCPlacemark]) { 399 | 400 | mapView.removeAnnotations(mapView.annotations) 401 | let annotations = viewModel.placemarks.enumerated().map { (arg0) -> HYCAnntation in 402 | let (offset, element) = arg0 403 | return HYCAnntation(placemark: element, sorted: offset + 1) 404 | } 405 | mapView.addAnnotations(annotations) 406 | 407 | 408 | tableView.reloadData() 409 | } 410 | 411 | func viewModel(_ viewModel: MapViewModel, isFetching: Bool) { 412 | if isFetching { 413 | HYCLoadingView.shared.show() 414 | } else { 415 | HYCLoadingView.shared.dismiss() 416 | } 417 | } 418 | 419 | func viewModel(_ viewModel: MapViewModel, didUpdatePolylines polylines: [MKPolyline]) { 420 | mapView.removeOverlays(mapView.overlays) 421 | mapView.addOverlays(polylines, level: .aboveRoads) 422 | if polylines.count > 0 { 423 | let rect = MapMananger.boundingMapRect(polylines: polylines) 424 | let verticalInset = mapView.frame.height / 10 425 | let horizatonInset = mapView.frame.width / 10 426 | let edgeInsets = UIEdgeInsets(top: verticalInset, left: horizatonInset, bottom: verticalInset + (heightOfUnit * 2), right: horizatonInset) // TODO: 88 為 lowestY, 應該綁在一起 427 | mapView.setVisibleMapRect(rect, edgePadding: edgeInsets, animated: false) 428 | } else { 429 | mapView.showAnnotations([mapView.userLocation], animated: true) 430 | } 431 | } 432 | 433 | func viewModel(_ viewModel: MapViewModel, didRecevice error: Error) { 434 | self.presentAlert(of: error.localizedDescription) 435 | } 436 | 437 | func viewModel(_ viewModel: MapViewModel, shouldShowTableView show: Bool) { 438 | func openMovableView() { 439 | UIView.animate(withDuration: 0.25) { 440 | self.movableViewTopToMapViewBottom.constant = self.switchOnConstantOfMovableView 441 | self.view.layoutIfNeeded() 442 | } 443 | } 444 | 445 | func closeMovableView() { 446 | UIView.animate(withDuration: 0.25) { 447 | self.movableViewTopToMapViewBottom.constant = self.switchOffConstantOfMovableView 448 | self.view.layoutIfNeeded() 449 | } 450 | } 451 | 452 | if show { 453 | openMovableView() 454 | } else { 455 | closeMovableView() 456 | } 457 | } 458 | } 459 | 460 | // MARK: - CLLocationManagerDelegate 461 | extension MapViewController: CLLocationManagerDelegate { 462 | 463 | private func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { 464 | if status != .authorizedWhenInUse { 465 | manager.requestLocation() 466 | } 467 | } 468 | 469 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 470 | mapView.setUserTrackingMode(.follow, animated: true) 471 | } 472 | 473 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 474 | print("manager didFailWithError: \(error.localizedDescription)") 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /SalesTraveling.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 080B855A24EE76D300C1833F /* DataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080B855924EE76D300C1833F /* DataManagerTests.swift */; }; 11 | 7E63348F207200BB007A2A85 /* UIAlertController+Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E63348E207200BB007A2A85 /* UIAlertController+Init.swift */; }; 12 | 7EB0D60A1F9E341B00268E3B /* TourModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB0D6091F9E341B00268E3B /* TourModel.swift */; }; 13 | 7EFBA2621F9C4B480011007E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EFBA2611F9C4B480011007E /* AppDelegate.swift */; }; 14 | 7EFBA2691F9C4B480011007E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7EFBA2681F9C4B480011007E /* Assets.xcassets */; }; 15 | 7EFBA26C1F9C4B480011007E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7EFBA26A1F9C4B480011007E /* LaunchScreen.storyboard */; }; 16 | 7EFBA2741F9C4D670011007E /* MapMananger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EFBA2731F9C4D670011007E /* MapMananger.swift */; }; 17 | 7EFBA2771F9C53080011007E /* Taipei 101.gpx in Resources */ = {isa = PBXBuildFile; fileRef = 7EFBA2761F9C53080011007E /* Taipei 101.gpx */; }; 18 | 7EFBA27D1F9C55490011007E /* AddressResultTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EFBA27C1F9C55490011007E /* AddressResultTableViewController.swift */; }; 19 | 7EFBA27F1F9C82B10011007E /* AddressResult.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7EFBA27E1F9C82B10011007E /* AddressResult.storyboard */; }; 20 | 9445682C1FC45E9A00D4C7FD /* DirectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9445682B1FC45E9A00D4C7FD /* DirectionModel.swift */; }; 21 | 94598E1F22BA834C003DD991 /* HYCPlacemark+PostalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94598E1E22BA834C003DD991 /* HYCPlacemark+PostalAddress.swift */; }; 22 | 94598E2522BA8930003DD991 /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94598E2422BA8930003DD991 /* UIViewController+Alert.swift */; }; 23 | 94598E2722BA8E41003DD991 /* MapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94598E2622BA8E41003DD991 /* MapViewModel.swift */; }; 24 | 9474E21520FF857B00A13F65 /* CLLocationCoordinate2D+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474E21420FF857B00A13F65 /* CLLocationCoordinate2D+Codable.swift */; }; 25 | 94798C1221E3A863004627BC /* Map.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 94798C1121E3A862004627BC /* Map.storyboard */; }; 26 | 94798C1521E3AB9C004627BC /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94798C1421E3AB9C004627BC /* MapViewController.swift */; }; 27 | 9481FCE020A7241200F8E842 /* HYCLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9481FCDF20A7241200F8E842 /* HYCLoadingView.swift */; }; 28 | 94822C8622B5402700DB4E88 /* HYCPlacemark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94822C8522B5402700DB4E88 /* HYCPlacemark.swift */; }; 29 | 94C476FD23EDB82F00A7B595 /* HYCAnntation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C476FC23EDB82F00A7B595 /* HYCAnntation.swift */; }; 30 | 94D00FFE1FC9B60A0003E979 /* UIColor+VisualIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D00FFD1FC9B60A0003E979 /* UIColor+VisualIdentity.swift */; }; 31 | 94D010021FC9D14D0003E979 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D010011FC9D14D0003E979 /* DataManager.swift */; }; 32 | 94D010041FC9D5490003E979 /* UserDefaults+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D010031FC9D5490003E979 /* UserDefaults+Keys.swift */; }; 33 | 94E2D1051FC6F4A400F1FD36 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 94E2D1071FC6F4A400F1FD36 /* Localizable.strings */; }; 34 | 94E2D10A1FC6F83700F1FD36 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E2D1091FC6F83700F1FD36 /* String+Localized.swift */; }; 35 | 94E2D10C1FC7019900F1FD36 /* NSObject+ClassName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E2D10B1FC7019900F1FD36 /* NSObject+ClassName.swift */; }; 36 | 94FB42891FB6E663005039B7 /* Array+Algorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94FB42881FB6E663005039B7 /* Array+Algorithm.swift */; }; 37 | 94FB428C1FB6E6A8005039B7 /* AlgorithmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94FB428B1FB6E6A8005039B7 /* AlgorithmManager.swift */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXContainerItemProxy section */ 41 | 084CED7924ED9FE0003B551C /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = 7EFBA2561F9C4B480011007E /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = 7EFBA25D1F9C4B480011007E; 46 | remoteInfo = SalesTraveling; 47 | }; 48 | /* End PBXContainerItemProxy section */ 49 | 50 | /* Begin PBXFileReference section */ 51 | 080B855924EE76D300C1833F /* DataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManagerTests.swift; sourceTree = ""; }; 52 | 084CED7424ED9FE0003B551C /* SalesTravelingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SalesTravelingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 084CED7824ED9FE0003B551C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | 7E06DA2C20514DAA0020A192 /* Cardino Blue.gpx */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Cardino Blue.gpx"; sourceTree = ""; }; 55 | 7E63348E207200BB007A2A85 /* UIAlertController+Init.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Init.swift"; sourceTree = ""; }; 56 | 7EB0D6091F9E341B00268E3B /* TourModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TourModel.swift; sourceTree = ""; }; 57 | 7EFBA25E1F9C4B480011007E /* SalesTraveling.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SalesTraveling.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 7EFBA2611F9C4B480011007E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | 7EFBA2681F9C4B480011007E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 60 | 7EFBA26B1F9C4B480011007E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 61 | 7EFBA26D1F9C4B480011007E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | 7EFBA2731F9C4D670011007E /* MapMananger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMananger.swift; sourceTree = ""; }; 63 | 7EFBA2761F9C53080011007E /* Taipei 101.gpx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "Taipei 101.gpx"; sourceTree = ""; }; 64 | 7EFBA27C1F9C55490011007E /* AddressResultTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressResultTableViewController.swift; sourceTree = ""; }; 65 | 7EFBA27E1F9C82B10011007E /* AddressResult.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = AddressResult.storyboard; sourceTree = ""; }; 66 | 9445682B1FC45E9A00D4C7FD /* DirectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionModel.swift; sourceTree = ""; }; 67 | 94598E1E22BA834C003DD991 /* HYCPlacemark+PostalAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HYCPlacemark+PostalAddress.swift"; sourceTree = ""; }; 68 | 94598E2422BA8930003DD991 /* UIViewController+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Alert.swift"; sourceTree = ""; }; 69 | 94598E2622BA8E41003DD991 /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = ""; }; 70 | 9474E21420FF857B00A13F65 /* CLLocationCoordinate2D+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLLocationCoordinate2D+Codable.swift"; sourceTree = ""; }; 71 | 94798C1121E3A862004627BC /* Map.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Map.storyboard; sourceTree = ""; }; 72 | 94798C1421E3AB9C004627BC /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; }; 73 | 9481FCDF20A7241200F8E842 /* HYCLoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HYCLoadingView.swift; sourceTree = ""; }; 74 | 94822C8522B5402700DB4E88 /* HYCPlacemark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HYCPlacemark.swift; sourceTree = ""; }; 75 | 94C476FC23EDB82F00A7B595 /* HYCAnntation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HYCAnntation.swift; sourceTree = ""; }; 76 | 94D00FFD1FC9B60A0003E979 /* UIColor+VisualIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VisualIdentity.swift"; sourceTree = ""; }; 77 | 94D010011FC9D14D0003E979 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; 78 | 94D010031FC9D5490003E979 /* UserDefaults+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Keys.swift"; sourceTree = ""; }; 79 | 94E2D1061FC6F4A400F1FD36 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 80 | 94E2D1081FC6F4AE00F1FD36 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 81 | 94E2D1091FC6F83700F1FD36 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; 82 | 94E2D10B1FC7019900F1FD36 /* NSObject+ClassName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+ClassName.swift"; sourceTree = ""; }; 83 | 94FB42881FB6E663005039B7 /* Array+Algorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Algorithm.swift"; sourceTree = ""; }; 84 | 94FB428B1FB6E6A8005039B7 /* AlgorithmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmManager.swift; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | 084CED7124ED9FE0003B551C /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | 7EFBA25B1F9C4B480011007E /* Frameworks */ = { 96 | isa = PBXFrameworksBuildPhase; 97 | buildActionMask = 2147483647; 98 | files = ( 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | /* End PBXFrameworksBuildPhase section */ 103 | 104 | /* Begin PBXGroup section */ 105 | 084CED7524ED9FE0003B551C /* SalesTravelingTests */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 084CED7824ED9FE0003B551C /* Info.plist */, 109 | 080B855924EE76D300C1833F /* DataManagerTests.swift */, 110 | ); 111 | path = SalesTravelingTests; 112 | sourceTree = ""; 113 | }; 114 | 7EB0D60B1F9E342100268E3B /* Models */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 9445682B1FC45E9A00D4C7FD /* DirectionModel.swift */, 118 | 94C476FC23EDB82F00A7B595 /* HYCAnntation.swift */, 119 | 94598E1E22BA834C003DD991 /* HYCPlacemark+PostalAddress.swift */, 120 | 94822C8522B5402700DB4E88 /* HYCPlacemark.swift */, 121 | 7EB0D6091F9E341B00268E3B /* TourModel.swift */, 122 | ); 123 | path = Models; 124 | sourceTree = ""; 125 | }; 126 | 7EFBA2551F9C4B480011007E = { 127 | isa = PBXGroup; 128 | children = ( 129 | 7EFBA25F1F9C4B480011007E /* Products */, 130 | 7EFBA2601F9C4B480011007E /* SalesTraveling */, 131 | 084CED7524ED9FE0003B551C /* SalesTravelingTests */, 132 | ); 133 | sourceTree = ""; 134 | }; 135 | 7EFBA25F1F9C4B480011007E /* Products */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 7EFBA25E1F9C4B480011007E /* SalesTraveling.app */, 139 | 084CED7424ED9FE0003B551C /* SalesTravelingTests.xctest */, 140 | ); 141 | name = Products; 142 | sourceTree = ""; 143 | }; 144 | 7EFBA2601F9C4B480011007E /* SalesTraveling */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 94E2D10F1FC713E400F1FD36 /* Custom UI */, 148 | 94FB428A1FB6E668005039B7 /* Extensions */, 149 | 7EFBA2841F9C8D8D0011007E /* Flows */, 150 | 9454065A23EF0EA60002319A /* GPXs */, 151 | 7EFBA2751F9C4D6E0011007E /* Managers */, 152 | 7EB0D60B1F9E342100268E3B /* Models */, 153 | 7EFBA2611F9C4B480011007E /* AppDelegate.swift */, 154 | 7EFBA2681F9C4B480011007E /* Assets.xcassets */, 155 | 7EFBA26D1F9C4B480011007E /* Info.plist */, 156 | 7EFBA26A1F9C4B480011007E /* LaunchScreen.storyboard */, 157 | 94E2D1071FC6F4A400F1FD36 /* Localizable.strings */, 158 | ); 159 | path = SalesTraveling; 160 | sourceTree = ""; 161 | }; 162 | 7EFBA2751F9C4D6E0011007E /* Managers */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 94FB428B1FB6E6A8005039B7 /* AlgorithmManager.swift */, 166 | 94D010011FC9D14D0003E979 /* DataManager.swift */, 167 | 7EFBA2731F9C4D670011007E /* MapMananger.swift */, 168 | ); 169 | path = Managers; 170 | sourceTree = ""; 171 | }; 172 | 7EFBA2821F9C8D730011007E /* Locate */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 7EFBA27E1F9C82B10011007E /* AddressResult.storyboard */, 176 | 7EFBA27C1F9C55490011007E /* AddressResultTableViewController.swift */, 177 | ); 178 | path = Locate; 179 | sourceTree = ""; 180 | }; 181 | 7EFBA2841F9C8D8D0011007E /* Flows */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 7EFBA2821F9C8D730011007E /* Locate */, 185 | 94798C1321E3A876004627BC /* Map */, 186 | ); 187 | path = Flows; 188 | sourceTree = ""; 189 | }; 190 | 9454065A23EF0EA60002319A /* GPXs */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 7E06DA2C20514DAA0020A192 /* Cardino Blue.gpx */, 194 | 7EFBA2761F9C53080011007E /* Taipei 101.gpx */, 195 | ); 196 | path = GPXs; 197 | sourceTree = ""; 198 | }; 199 | 94798C1321E3A876004627BC /* Map */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | 94798C1121E3A862004627BC /* Map.storyboard */, 203 | 94798C1421E3AB9C004627BC /* MapViewController.swift */, 204 | 94598E2622BA8E41003DD991 /* MapViewModel.swift */, 205 | ); 206 | path = Map; 207 | sourceTree = ""; 208 | }; 209 | 94E2D10F1FC713E400F1FD36 /* Custom UI */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 9481FCDF20A7241200F8E842 /* HYCLoadingView.swift */, 213 | ); 214 | path = "Custom UI"; 215 | sourceTree = ""; 216 | }; 217 | 94FB428A1FB6E668005039B7 /* Extensions */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | 94FB42881FB6E663005039B7 /* Array+Algorithm.swift */, 221 | 9474E21420FF857B00A13F65 /* CLLocationCoordinate2D+Codable.swift */, 222 | 94E2D10B1FC7019900F1FD36 /* NSObject+ClassName.swift */, 223 | 94E2D1091FC6F83700F1FD36 /* String+Localized.swift */, 224 | 7E63348E207200BB007A2A85 /* UIAlertController+Init.swift */, 225 | 94D00FFD1FC9B60A0003E979 /* UIColor+VisualIdentity.swift */, 226 | 94598E2422BA8930003DD991 /* UIViewController+Alert.swift */, 227 | 94D010031FC9D5490003E979 /* UserDefaults+Keys.swift */, 228 | ); 229 | path = Extensions; 230 | sourceTree = ""; 231 | }; 232 | /* End PBXGroup section */ 233 | 234 | /* Begin PBXNativeTarget section */ 235 | 084CED7324ED9FE0003B551C /* SalesTravelingTests */ = { 236 | isa = PBXNativeTarget; 237 | buildConfigurationList = 084CED7D24ED9FE0003B551C /* Build configuration list for PBXNativeTarget "SalesTravelingTests" */; 238 | buildPhases = ( 239 | 084CED7024ED9FE0003B551C /* Sources */, 240 | 084CED7124ED9FE0003B551C /* Frameworks */, 241 | 084CED7224ED9FE0003B551C /* Resources */, 242 | ); 243 | buildRules = ( 244 | ); 245 | dependencies = ( 246 | 084CED7A24ED9FE0003B551C /* PBXTargetDependency */, 247 | ); 248 | name = SalesTravelingTests; 249 | productName = SalesTravelingTests; 250 | productReference = 084CED7424ED9FE0003B551C /* SalesTravelingTests.xctest */; 251 | productType = "com.apple.product-type.bundle.unit-test"; 252 | }; 253 | 7EFBA25D1F9C4B480011007E /* SalesTraveling */ = { 254 | isa = PBXNativeTarget; 255 | buildConfigurationList = 7EFBA2701F9C4B480011007E /* Build configuration list for PBXNativeTarget "SalesTraveling" */; 256 | buildPhases = ( 257 | 7EFBA25A1F9C4B480011007E /* Sources */, 258 | 7EFBA25B1F9C4B480011007E /* Frameworks */, 259 | 7EFBA25C1F9C4B480011007E /* Resources */, 260 | ); 261 | buildRules = ( 262 | ); 263 | dependencies = ( 264 | ); 265 | name = SalesTraveling; 266 | productName = SalesTraveling; 267 | productReference = 7EFBA25E1F9C4B480011007E /* SalesTraveling.app */; 268 | productType = "com.apple.product-type.application"; 269 | }; 270 | /* End PBXNativeTarget section */ 271 | 272 | /* Begin PBXProject section */ 273 | 7EFBA2561F9C4B480011007E /* Project object */ = { 274 | isa = PBXProject; 275 | attributes = { 276 | LastSwiftUpdateCheck = 1160; 277 | LastUpgradeCheck = 0940; 278 | ORGANIZATIONNAME = Hanyu; 279 | TargetAttributes = { 280 | 084CED7324ED9FE0003B551C = { 281 | CreatedOnToolsVersion = 11.6; 282 | LastSwiftMigration = 1160; 283 | ProvisioningStyle = Manual; 284 | TestTargetID = 7EFBA25D1F9C4B480011007E; 285 | }; 286 | 7EFBA25D1F9C4B480011007E = { 287 | CreatedOnToolsVersion = 9.0.1; 288 | LastSwiftMigration = 1020; 289 | ProvisioningStyle = Manual; 290 | }; 291 | }; 292 | }; 293 | buildConfigurationList = 7EFBA2591F9C4B480011007E /* Build configuration list for PBXProject "SalesTraveling" */; 294 | compatibilityVersion = "Xcode 8.0"; 295 | developmentRegion = en; 296 | hasScannedForEncodings = 0; 297 | knownRegions = ( 298 | en, 299 | Base, 300 | "zh-Hant", 301 | ); 302 | mainGroup = 7EFBA2551F9C4B480011007E; 303 | productRefGroup = 7EFBA25F1F9C4B480011007E /* Products */; 304 | projectDirPath = ""; 305 | projectRoot = ""; 306 | targets = ( 307 | 7EFBA25D1F9C4B480011007E /* SalesTraveling */, 308 | 084CED7324ED9FE0003B551C /* SalesTravelingTests */, 309 | ); 310 | }; 311 | /* End PBXProject section */ 312 | 313 | /* Begin PBXResourcesBuildPhase section */ 314 | 084CED7224ED9FE0003B551C /* Resources */ = { 315 | isa = PBXResourcesBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | 7EFBA25C1F9C4B480011007E /* Resources */ = { 322 | isa = PBXResourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | 7EFBA27F1F9C82B10011007E /* AddressResult.storyboard in Resources */, 326 | 7EFBA2691F9C4B480011007E /* Assets.xcassets in Resources */, 327 | 7EFBA26C1F9C4B480011007E /* LaunchScreen.storyboard in Resources */, 328 | 94E2D1051FC6F4A400F1FD36 /* Localizable.strings in Resources */, 329 | 94798C1221E3A863004627BC /* Map.storyboard in Resources */, 330 | 7EFBA2771F9C53080011007E /* Taipei 101.gpx in Resources */, 331 | ); 332 | runOnlyForDeploymentPostprocessing = 0; 333 | }; 334 | /* End PBXResourcesBuildPhase section */ 335 | 336 | /* Begin PBXSourcesBuildPhase section */ 337 | 084CED7024ED9FE0003B551C /* Sources */ = { 338 | isa = PBXSourcesBuildPhase; 339 | buildActionMask = 2147483647; 340 | files = ( 341 | 080B855A24EE76D300C1833F /* DataManagerTests.swift in Sources */, 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | 7EFBA25A1F9C4B480011007E /* Sources */ = { 346 | isa = PBXSourcesBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | 7EFBA27D1F9C55490011007E /* AddressResultTableViewController.swift in Sources */, 350 | 94FB428C1FB6E6A8005039B7 /* AlgorithmManager.swift in Sources */, 351 | 7EFBA2621F9C4B480011007E /* AppDelegate.swift in Sources */, 352 | 94FB42891FB6E663005039B7 /* Array+Algorithm.swift in Sources */, 353 | 9474E21520FF857B00A13F65 /* CLLocationCoordinate2D+Codable.swift in Sources */, 354 | 94D010021FC9D14D0003E979 /* DataManager.swift in Sources */, 355 | 9445682C1FC45E9A00D4C7FD /* DirectionModel.swift in Sources */, 356 | 94C476FD23EDB82F00A7B595 /* HYCAnntation.swift in Sources */, 357 | 9481FCE020A7241200F8E842 /* HYCLoadingView.swift in Sources */, 358 | 94598E1F22BA834C003DD991 /* HYCPlacemark+PostalAddress.swift in Sources */, 359 | 94822C8622B5402700DB4E88 /* HYCPlacemark.swift in Sources */, 360 | 7EFBA2741F9C4D670011007E /* MapMananger.swift in Sources */, 361 | 94798C1521E3AB9C004627BC /* MapViewController.swift in Sources */, 362 | 94598E2722BA8E41003DD991 /* MapViewModel.swift in Sources */, 363 | 94E2D10C1FC7019900F1FD36 /* NSObject+ClassName.swift in Sources */, 364 | 94E2D10A1FC6F83700F1FD36 /* String+Localized.swift in Sources */, 365 | 7EB0D60A1F9E341B00268E3B /* TourModel.swift in Sources */, 366 | 7E63348F207200BB007A2A85 /* UIAlertController+Init.swift in Sources */, 367 | 94D00FFE1FC9B60A0003E979 /* UIColor+VisualIdentity.swift in Sources */, 368 | 94598E2522BA8930003DD991 /* UIViewController+Alert.swift in Sources */, 369 | 94D010041FC9D5490003E979 /* UserDefaults+Keys.swift in Sources */, 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | /* End PBXSourcesBuildPhase section */ 374 | 375 | /* Begin PBXTargetDependency section */ 376 | 084CED7A24ED9FE0003B551C /* PBXTargetDependency */ = { 377 | isa = PBXTargetDependency; 378 | target = 7EFBA25D1F9C4B480011007E /* SalesTraveling */; 379 | targetProxy = 084CED7924ED9FE0003B551C /* PBXContainerItemProxy */; 380 | }; 381 | /* End PBXTargetDependency section */ 382 | 383 | /* Begin PBXVariantGroup section */ 384 | 7EFBA26A1F9C4B480011007E /* LaunchScreen.storyboard */ = { 385 | isa = PBXVariantGroup; 386 | children = ( 387 | 7EFBA26B1F9C4B480011007E /* Base */, 388 | ); 389 | name = LaunchScreen.storyboard; 390 | sourceTree = ""; 391 | }; 392 | 94E2D1071FC6F4A400F1FD36 /* Localizable.strings */ = { 393 | isa = PBXVariantGroup; 394 | children = ( 395 | 94E2D1061FC6F4A400F1FD36 /* en */, 396 | 94E2D1081FC6F4AE00F1FD36 /* zh-Hant */, 397 | ); 398 | name = Localizable.strings; 399 | sourceTree = ""; 400 | }; 401 | /* End PBXVariantGroup section */ 402 | 403 | /* Begin XCBuildConfiguration section */ 404 | 084CED7B24ED9FE0003B551C /* Debug */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | BUNDLE_LOADER = "$(TEST_HOST)"; 408 | CLANG_ENABLE_MODULES = YES; 409 | CLANG_ENABLE_OBJC_WEAK = YES; 410 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 411 | CODE_SIGN_STYLE = Manual; 412 | DEVELOPMENT_TEAM = 9LB9NKBAM6; 413 | INFOPLIST_FILE = SalesTravelingTests/Info.plist; 414 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 415 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 416 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 417 | MTL_FAST_MATH = YES; 418 | PRODUCT_BUNDLE_IDENTIFIER = tw.com.shodo.tests; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | PROVISIONING_PROFILE_SPECIFIER = ""; 421 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 422 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 423 | SWIFT_VERSION = 5.0; 424 | TARGETED_DEVICE_FAMILY = "1,2"; 425 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SalesTraveling.app/SalesTraveling"; 426 | }; 427 | name = Debug; 428 | }; 429 | 084CED7C24ED9FE0003B551C /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | BUNDLE_LOADER = "$(TEST_HOST)"; 433 | CLANG_ENABLE_MODULES = YES; 434 | CLANG_ENABLE_OBJC_WEAK = YES; 435 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 436 | CODE_SIGN_STYLE = Manual; 437 | DEVELOPMENT_TEAM = 9LB9NKBAM6; 438 | INFOPLIST_FILE = SalesTravelingTests/Info.plist; 439 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 441 | MTL_FAST_MATH = YES; 442 | PRODUCT_BUNDLE_IDENTIFIER = tw.com.shodo.tests; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | PROVISIONING_PROFILE_SPECIFIER = ""; 445 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 446 | SWIFT_VERSION = 5.0; 447 | TARGETED_DEVICE_FAMILY = "1,2"; 448 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SalesTraveling.app/SalesTraveling"; 449 | }; 450 | name = Release; 451 | }; 452 | 7EFBA26E1F9C4B480011007E /* Debug */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | ALWAYS_SEARCH_USER_PATHS = NO; 456 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 459 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 460 | CLANG_CXX_LIBRARY = "libc++"; 461 | CLANG_ENABLE_MODULES = YES; 462 | CLANG_ENABLE_OBJC_ARC = YES; 463 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 464 | CLANG_WARN_BOOL_CONVERSION = YES; 465 | CLANG_WARN_COMMA = YES; 466 | CLANG_WARN_CONSTANT_CONVERSION = YES; 467 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 468 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 469 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 470 | CLANG_WARN_EMPTY_BODY = YES; 471 | CLANG_WARN_ENUM_CONVERSION = YES; 472 | CLANG_WARN_INFINITE_RECURSION = YES; 473 | CLANG_WARN_INT_CONVERSION = YES; 474 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 476 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 477 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 478 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 479 | CLANG_WARN_STRICT_PROTOTYPES = YES; 480 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 481 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 482 | CLANG_WARN_UNREACHABLE_CODE = YES; 483 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 484 | CODE_SIGN_IDENTITY = "iPhone Developer"; 485 | COPY_PHASE_STRIP = NO; 486 | DEBUG_INFORMATION_FORMAT = dwarf; 487 | ENABLE_STRICT_OBJC_MSGSEND = YES; 488 | ENABLE_TESTABILITY = YES; 489 | GCC_C_LANGUAGE_STANDARD = gnu11; 490 | GCC_DYNAMIC_NO_PIC = NO; 491 | GCC_NO_COMMON_BLOCKS = YES; 492 | GCC_OPTIMIZATION_LEVEL = 0; 493 | GCC_PREPROCESSOR_DEFINITIONS = ( 494 | "DEBUG=1", 495 | "$(inherited)", 496 | ); 497 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 498 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 499 | GCC_WARN_UNDECLARED_SELECTOR = YES; 500 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 501 | GCC_WARN_UNUSED_FUNCTION = YES; 502 | GCC_WARN_UNUSED_VARIABLE = YES; 503 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 504 | MARKETING_VERSION = ""; 505 | MTL_ENABLE_DEBUG_INFO = YES; 506 | ONLY_ACTIVE_ARCH = YES; 507 | SDKROOT = iphoneos; 508 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 509 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 510 | }; 511 | name = Debug; 512 | }; 513 | 7EFBA26F1F9C4B480011007E /* Release */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | ALWAYS_SEARCH_USER_PATHS = NO; 517 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 518 | CLANG_ANALYZER_NONNULL = YES; 519 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 520 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 521 | CLANG_CXX_LIBRARY = "libc++"; 522 | CLANG_ENABLE_MODULES = YES; 523 | CLANG_ENABLE_OBJC_ARC = YES; 524 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 525 | CLANG_WARN_BOOL_CONVERSION = YES; 526 | CLANG_WARN_COMMA = YES; 527 | CLANG_WARN_CONSTANT_CONVERSION = YES; 528 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 529 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 530 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 531 | CLANG_WARN_EMPTY_BODY = YES; 532 | CLANG_WARN_ENUM_CONVERSION = YES; 533 | CLANG_WARN_INFINITE_RECURSION = YES; 534 | CLANG_WARN_INT_CONVERSION = YES; 535 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 536 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 537 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 538 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 539 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 540 | CLANG_WARN_STRICT_PROTOTYPES = YES; 541 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 542 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 543 | CLANG_WARN_UNREACHABLE_CODE = YES; 544 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 545 | CODE_SIGN_IDENTITY = "iPhone Developer"; 546 | COPY_PHASE_STRIP = NO; 547 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 548 | ENABLE_NS_ASSERTIONS = NO; 549 | ENABLE_STRICT_OBJC_MSGSEND = YES; 550 | GCC_C_LANGUAGE_STANDARD = gnu11; 551 | GCC_NO_COMMON_BLOCKS = YES; 552 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 553 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 554 | GCC_WARN_UNDECLARED_SELECTOR = YES; 555 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 556 | GCC_WARN_UNUSED_FUNCTION = YES; 557 | GCC_WARN_UNUSED_VARIABLE = YES; 558 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 559 | MARKETING_VERSION = ""; 560 | MTL_ENABLE_DEBUG_INFO = NO; 561 | SDKROOT = iphoneos; 562 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 563 | VALIDATE_PRODUCT = YES; 564 | }; 565 | name = Release; 566 | }; 567 | 7EFBA2711F9C4B480011007E /* Debug */ = { 568 | isa = XCBuildConfiguration; 569 | buildSettings = { 570 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 571 | CODE_SIGN_STYLE = Manual; 572 | DEVELOPMENT_TEAM = 9LB9NKBAM6; 573 | INFOPLIST_FILE = SalesTraveling/Info.plist; 574 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 575 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 576 | MARKETING_VERSION = 2.2.1; 577 | PRODUCT_BUNDLE_IDENTIFIER = tw.com.shodo; 578 | PRODUCT_NAME = SalesTraveling; 579 | PROVISIONING_PROFILE = "bd40195c-5ef6-4e44-9244-5b8308f9b92f"; 580 | PROVISIONING_PROFILE_SPECIFIER = Shodo_development; 581 | SWIFT_VERSION = 5.0; 582 | TARGETED_DEVICE_FAMILY = 1; 583 | }; 584 | name = Debug; 585 | }; 586 | 7EFBA2721F9C4B480011007E /* Release */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 590 | CODE_SIGN_IDENTITY = "iPhone Distribution"; 591 | CODE_SIGN_STYLE = Manual; 592 | DEVELOPMENT_TEAM = 9LB9NKBAM6; 593 | INFOPLIST_FILE = SalesTraveling/Info.plist; 594 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 595 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 596 | MARKETING_VERSION = 2.2.1; 597 | PRODUCT_BUNDLE_IDENTIFIER = tw.com.shodo; 598 | PRODUCT_NAME = SalesTraveling; 599 | PROVISIONING_PROFILE = "b9e3d4fe-d4e6-43d5-93b4-44e03b8ace37"; 600 | PROVISIONING_PROFILE_SPECIFIER = Shodo_distribution; 601 | SWIFT_VERSION = 5.0; 602 | TARGETED_DEVICE_FAMILY = 1; 603 | }; 604 | name = Release; 605 | }; 606 | /* End XCBuildConfiguration section */ 607 | 608 | /* Begin XCConfigurationList section */ 609 | 084CED7D24ED9FE0003B551C /* Build configuration list for PBXNativeTarget "SalesTravelingTests" */ = { 610 | isa = XCConfigurationList; 611 | buildConfigurations = ( 612 | 084CED7B24ED9FE0003B551C /* Debug */, 613 | 084CED7C24ED9FE0003B551C /* Release */, 614 | ); 615 | defaultConfigurationIsVisible = 0; 616 | defaultConfigurationName = Release; 617 | }; 618 | 7EFBA2591F9C4B480011007E /* Build configuration list for PBXProject "SalesTraveling" */ = { 619 | isa = XCConfigurationList; 620 | buildConfigurations = ( 621 | 7EFBA26E1F9C4B480011007E /* Debug */, 622 | 7EFBA26F1F9C4B480011007E /* Release */, 623 | ); 624 | defaultConfigurationIsVisible = 0; 625 | defaultConfigurationName = Release; 626 | }; 627 | 7EFBA2701F9C4B480011007E /* Build configuration list for PBXNativeTarget "SalesTraveling" */ = { 628 | isa = XCConfigurationList; 629 | buildConfigurations = ( 630 | 7EFBA2711F9C4B480011007E /* Debug */, 631 | 7EFBA2721F9C4B480011007E /* Release */, 632 | ); 633 | defaultConfigurationIsVisible = 0; 634 | defaultConfigurationName = Release; 635 | }; 636 | /* End XCConfigurationList section */ 637 | }; 638 | rootObject = 7EFBA2561F9C4B480011007E /* Project object */; 639 | } 640 | --------------------------------------------------------------------------------