├── 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 | # 
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 |
--------------------------------------------------------------------------------