├── MVVM ├── Assets.xcassets │ ├── Contents.json │ ├── NoData.imageset │ │ ├── NoData.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Extension │ ├── ArrayExtension.swift │ ├── Tools.swift │ └── UIColorExtension.swift ├── Model │ ├── ZipModel.swift │ ├── GarbageStationInfoModel.swift │ └── ClassModel.swift ├── View │ ├── TableViewCell │ │ ├── SchoolDataCell.swift │ │ └── GarbageStationInfoCell.swift │ └── CollectionViewCell │ │ └── ZipCollectionViewCell.swift ├── Base │ ├── Protocol │ │ ├── ViewModelStatusEnum.swift │ │ ├── ViewModelDelegate.swift │ │ ├── ViewModelProtocol.swift │ │ ├── BaseDataProtocol.swift │ │ ├── ViewModelMultipleContentDataProtocol.swift │ │ └── JsonProtocol.swift │ ├── View │ │ ├── NoDataView.swift │ │ └── EmptyView.swift │ ├── Error.swift │ ├── ViewController │ │ └── BaseVC.swift │ └── ViewModel │ │ └── BaseViewModel.swift ├── ViewModel │ ├── ZipViewModel.swift │ ├── SchoolDataViewModel.swift │ └── GarbageStationInfoViewModel.swift ├── AppDelegate.swift ├── data.json ├── ViewController │ ├── MainVC.swift │ ├── GarbageStationInfoVC.swift │ ├── ZipVC.swift │ └── SchoolDataVC.swift ├── Info.plist ├── Base.lproj │ └── LaunchScreen.storyboard └── Services │ └── WebService │ ├── ChlURLProtocol.swift │ └── WebService.swift ├── MVVM.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Podfile ├── .swiftlint.yml ├── MVVM.xcworkspace └── contents.xcworkspacedata ├── Podfile.lock ├── LICENSE ├── README.md ├── .gitignore └── sortXcodeProject.pl /MVVM/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MVVM/Assets.xcassets/NoData.imageset/NoData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nick6969/MVVM/HEAD/MVVM/Assets.xcassets/NoData.imageset/NoData.png -------------------------------------------------------------------------------- /MVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | 3 | inhibit_all_warnings! 4 | 5 | target 'MVVM' do 6 | 7 | use_frameworks! 8 | 9 | pod 'Alamofire', '4.7.1' 10 | pod 'mLayout', '0.1.2' 11 | pod 'SVProgressHUD', '2.2.5' 12 | 13 | end 14 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | # you can use - 'swiftlint rules' find 3 | - trailing_whitespace 4 | - force_cast 5 | opt_in_rules: 6 | # you can use - 'swiftlint rules' find 7 | 8 | included: 9 | 10 | excluded: 11 | - Pods 12 | 13 | line_length: 300 14 | function_body_length: 100 -------------------------------------------------------------------------------- /MVVM.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MVVM/Extension/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/7. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | subscript(safe index: Int) -> Element? { 13 | return (0 <= index && index < count) ? self[index] : nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MVVM/Model/ZipModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ZipModel: JsonModel { 12 | let district, zipcode: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case district = "District" 16 | case zipcode 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MVVM/View/TableViewCell/SchoolDataCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchoolDataCell.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/7. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SchoolDataCell: UITableViewCell { 12 | 13 | func setup(with model: StudentModel) { 14 | textLabel?.text = "座號: \(model.seatNumber), 姓名: \(model.name)" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MVVM/Assets.xcassets/NoData.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "NoData.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MVVM/Extension/Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tools.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public func print(msg: T, file: String = #file, method: String = #function, line: Int = #line) { 12 | #if DEBUG 13 | Swift.print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(msg)") 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /MVVM/View/TableViewCell/GarbageStationInfoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GarbageStationInfoCell.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GarbageStationInfoCell: UITableViewCell { 12 | 13 | func setup(with model: GarbageStationInfoModel) { 14 | textLabel?.text = model.linename + ", " + model.time + ", " + model.name 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /MVVM/Base/Protocol/ViewModelStatusEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelStatusEnum.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum ViewModelStatus { 12 | case initialize 13 | case loadStart 14 | case loadDone 15 | case loadFail 16 | case loadMoreStart 17 | case loadMoreDone 18 | case loadMoreFail 19 | case noMoreCanLoad 20 | case refreshLoading 21 | } 22 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.7.1) 3 | - mLayout (0.1.2) 4 | - SVProgressHUD (2.2.5) 5 | 6 | DEPENDENCIES: 7 | - Alamofire (= 4.7.1) 8 | - mLayout (= 0.1.2) 9 | - SVProgressHUD (= 2.2.5) 10 | 11 | SPEC CHECKSUMS: 12 | Alamofire: 68d7d521118d49c615a8d2214d87cdf525599d30 13 | mLayout: 7fa679d0da8636d2f93a73a41acf1c860190dc4d 14 | SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 15 | 16 | PODFILE CHECKSUM: c9da9087f274c2cdb994940953fb6dfc851f65ec 17 | 18 | COCOAPODS: 1.4.0 19 | -------------------------------------------------------------------------------- /MVVM/ViewModel/ZipViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipViewModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ZipViewModel: BaseViewModel, StandardViewModel { 12 | 13 | func loadData() { 14 | WebService.shared.getZipData(success: dataConvertToModelsClosure, fail: loadingFailClosure) 15 | } 16 | 17 | func loadDataMore() { 18 | status = .noMoreCanLoad 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /MVVM/Base/Protocol/ViewModelDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelDelegate.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewModelLoadingDelegate: class { 12 | func loadingDone() 13 | func loadingFail(_ error: Error?) 14 | } 15 | 16 | protocol ViewModelLoadingStatusDelegate: class { 17 | func showEmptyView(with: Error?) 18 | func removeEmptyView() 19 | func showLoading(_ bool: Bool) 20 | } 21 | -------------------------------------------------------------------------------- /MVVM/Model/GarbageStationInfoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GarbageStationInfoModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GarbageStationInfoModel: JsonModel { 12 | let city, lineid, linename, rank, name, village, longitude, latitude, time, memo: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case city, lineid, linename, rank, name, village, longitude, latitude, time, memo 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVM/ViewModel/SchoolDataViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchoolDataViewModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/7. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SchoolDataViewModel: BaseViewModel, StandardMultipleContentViewModel { 12 | 13 | func loadData() { 14 | WebService.shared.getSchoolData(success: dataConvertToModelsClosure, fail: loadingFailClosure) 15 | } 16 | 17 | func loadDataMore() { 18 | status = .noMoreCanLoad 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /MVVM/Model/ClassModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/7. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ClassModel: MultipleContentProtocol { 12 | let className, tutorName: String 13 | var subModels: [StudentModel] 14 | enum CodingKeys: String, CodingKey { 15 | case subModels = "students" 16 | case className 17 | case tutorName 18 | } 19 | } 20 | 21 | struct StudentModel: JsonModel { 22 | let name: String 23 | let seatNumber: Int 24 | } 25 | -------------------------------------------------------------------------------- /MVVM/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import mLayout 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | 19 | window = UIWindow(frame: UIScreen.main.bounds) 20 | window?.makeKeyAndVisible() 21 | window?.backgroundColor = .white 22 | window?.rootViewController = UINavigationController(rootViewController: MainVC()) 23 | 24 | return true 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /MVVM/Extension/UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColorExtension.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | convenience init(red: Int, green: Int, blue: Int) { 13 | assert(red >= 0 && red <= 255, "Invalid red component") 14 | assert(green >= 0 && green <= 255, "Invalid green component") 15 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 16 | 17 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 18 | } 19 | 20 | convenience init(netHex: Int) { 21 | self.init(red: (netHex >> 16) & 0xff, green: (netHex >> 8) & 0xff, blue: netHex & 0xff) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVM/ViewModel/GarbageStationInfoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GarbageStationInfoViewModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class GarbageStationInfoViewModel: BaseViewModel, StandardViewModel { 12 | 13 | fileprivate var area: String = String() 14 | 15 | convenience init(with area: String) { 16 | self.init() 17 | self.area = area 18 | } 19 | 20 | func loadData() { 21 | useLoadData() 22 | } 23 | 24 | func loadDataMore() { 25 | useLoadData() 26 | } 27 | 28 | private func useLoadData() { 29 | WebService.shared.getGarbageStationInfo(with: self.area, skip: self.datasCount, success: dataConvertToModelsClosure, fail: loadingFailClosure) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nick Lin 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 | -------------------------------------------------------------------------------- /MVVM/View/CollectionViewCell/ZipCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipCollectionViewCell.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ZipCollectionViewCell: UICollectionViewCell { 12 | 13 | fileprivate let label = UILabel() 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | setupUI() 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | super.init(coder: aDecoder) 22 | setupUI() 23 | } 24 | 25 | private func setupUI() { 26 | contentView.addSubview(label) 27 | label.mLay(pin: .zero) 28 | label.textAlignment = .center 29 | label.font = UIFont.systemFont(ofSize: 18) 30 | contentView.layer.borderWidth = 2 31 | contentView.layer.borderColor = UIColor.init(netHex: 0x747474).cgColor 32 | contentView.layer.masksToBounds = true 33 | contentView.layer.cornerRadius = 10 34 | contentView.backgroundColor = .white 35 | } 36 | 37 | func setup(with model: ZipModel) { 38 | self.label.text = model.district 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /MVVM/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "className": "八年七班", 4 | "tutorName": "凌凌漆", 5 | "students": [ 6 | { 7 | "name": "天字第一號", 8 | "seatNumber": 1 9 | }, 10 | { 11 | "name": "雙雙對對", 12 | "seatNumber": 2 13 | }, 14 | { 15 | "name": "三人成群", 16 | "seatNumber": 3 17 | }, 18 | { 19 | "name": "亞洲四小龍", 20 | "seatNumber": 4 21 | } 22 | ] 23 | }, 24 | { 25 | "className": "一年二班", 26 | "tutorName": "黑黑黑", 27 | "students": [ 28 | { 29 | "name": "😱😱😱", 30 | "seatNumber": 1 31 | }, 32 | { 33 | "name": "💀💀💀💀", 34 | "seatNumber": 2 35 | }, 36 | { 37 | "name": "😡😡😡😡", 38 | "seatNumber": 3 39 | }, 40 | { 41 | "name": "🐽🐽🐽🐽", 42 | "seatNumber": 4 43 | }, 44 | { 45 | "name": "🌪🌪🌪🌪", 46 | "seatNumber": 5 47 | } 48 | ] 49 | } 50 | 51 | ] -------------------------------------------------------------------------------- /MVVM/Base/View/NoDataView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoDataView.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NoDataView: UIView { 12 | let imageView = UIImageView() 13 | let messageLabel = UILabel() 14 | required init?(coder aDecoder: NSCoder) { 15 | super.init(coder: aDecoder) 16 | setupUI() 17 | } 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | setupUI() 22 | } 23 | 24 | private func setupUI() { 25 | backgroundColor = UIColor.init(netHex: 0xf6f6f6) 26 | addSubview(imageView) 27 | addSubview(messageLabel) 28 | messageLabel.font = UIFont.systemFont(ofSize: 18) 29 | messageLabel.textAlignment = .center 30 | messageLabel.text = "目前沒有任何內容" 31 | messageLabel.numberOfLines = 1 32 | imageView.image = UIImage(named: "NoData") 33 | imageView.contentMode = .scaleAspectFit 34 | 35 | imageView.mLay(size: CGSize(width: 180, height: 100)) 36 | imageView.mLay(.centerX, .equal, self) 37 | imageView.mLay(.centerY, .equal, self, multiplier: 0.8, constant: 0) 38 | 39 | messageLabel.mLay(.top, .equal, self, .centerY, multiplier: 1.2, constant: 0) 40 | messageLabel.mLay(.centerX, .equal, self) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 關於 2 | 3 | 這是一個 MVVM 的展示專案 4 | 5 | ViewModel 使用 POP & OOP 建立 6 | 7 | 方便使用,並且減少大量重複的 Code 8 | 9 | 10 | -- 11 | 12 | # 架構 13 | 14 | ### Model 15 | Just Model 16 | 17 | -------------- 18 | 19 | ### ViewModel 20 | #### 持有 Models 21 | var models: [Model] 22 | 23 | #### 有自己的狀態 24 | var status: ViewModelStatus 25 | 26 | #### 負責取得資料(Web Api / Locale DB) 27 | func loadData() 28 | func loadDataMode() 29 | 30 | #### 負責通知 ViewController Loading 狀態 31 | var loadingDelegate: ViewModelLoadingDelegate? 32 | var loadingStatusDelegate: ViewModelLoadingStatusDelegate? 33 | 34 | #### 提供 Method 讓 ViewController 取用 Model 35 | var datasCount: Int 36 | func model(at index: Int) -> Model? 37 | func isLastData(index: Int) -> Bool 38 | func isLoadMore(index: Int) -> Bool 39 | 40 | #### 提供 Method 讓 ViewController 呼叫做事 41 | func refreshData() 42 | func nextStatus() 43 | 44 | -------------- 45 | 46 | ### ViewController 47 | #### 持有 ViewModel 48 | var viewModel: BaseViewModel 49 | 50 | #### conform ViewModel 的 Delegate 51 | extension ViewController: ViewModelLoadingDelegate 52 | 53 | func loadingDone() 54 | func loadingFail(_ error: Error?) 55 | 56 | extension ViewController: ViewModelLoadingStatusDelegate 57 | 58 | func showEmptyView(with: Error?) 59 | func removeEmptyView() 60 | func showLoading(_ bool: Bool) 61 | 62 | #### 請 ViewModel 做事 63 | viewModel.nextStatus() 64 | viewModel.refreshData() 65 | -------------------------------------------------------------------------------- /MVVM/Base/Protocol/ViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelProtocol.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias StandardViewModel = ViewModelDataProtocol & ViewModelLoadingProtocol & ViewModelLoadingFuncProtocol 12 | 13 | protocol ViewModelDataProtocol: BaseDataProtocol where Model: JsonModel { 14 | var datasCount: Int { get } 15 | func model(at index: Int) -> Model? 16 | func isLastData(index: Int) -> Bool 17 | func isLoadMore(index: Int) -> Bool 18 | } 19 | 20 | extension ViewModelDataProtocol { 21 | 22 | var datasCount: Int { return models.count } 23 | 24 | func model(at index: Int) -> Model? { 25 | return models[safe: index] 26 | } 27 | 28 | func isLastData(index: Int) -> Bool { 29 | return index + 1 == datasCount 30 | } 31 | 32 | func isLoadMore(index: Int) -> Bool { 33 | if models.isEmpty { return false } 34 | return index == datasCount 35 | } 36 | 37 | } 38 | 39 | protocol ViewModelLoadingProtocol: class { 40 | var loadingDelegate: ViewModelLoadingDelegate? {get set} 41 | var loadingStatusDelegate: ViewModelLoadingStatusDelegate? {get set} 42 | var status: ViewModelStatus { get set } 43 | func refreshData() 44 | func nextStatus() 45 | } 46 | 47 | protocol ViewModelLoadingFuncProtocol: class { 48 | func loadData() 49 | func loadDataMore() 50 | } 51 | -------------------------------------------------------------------------------- /MVVM/Base/Protocol/BaseDataProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseDataProtocol.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/10. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol BaseDataProtocol: class { 12 | associatedtype Model 13 | var models: [Model] { get set } 14 | } 15 | 16 | extension ViewModelLoadingProtocol where Self: BaseDataProtocol, Self: ViewModelLoadingFuncProtocol { 17 | 18 | func refreshData() { 19 | if status != .loadStart { 20 | status = .refreshLoading 21 | nextStatus() 22 | } 23 | } 24 | 25 | func nextStatus() { 26 | switch status { 27 | case .initialize, .loadFail: 28 | status = .loadStart 29 | if models.isEmpty { 30 | DispatchQueue.main.async { 31 | self.loadingStatusDelegate?.showLoading(true) 32 | } 33 | } 34 | loadData() 35 | 36 | case .loadDone, .loadMoreDone, .loadMoreFail: 37 | status = .loadMoreStart 38 | loadDataMore() 39 | 40 | case .refreshLoading: 41 | status = .loadStart 42 | models = [] 43 | DispatchQueue.main.async { 44 | self.loadingStatusDelegate?.showLoading(true) 45 | } 46 | loadData() 47 | 48 | case .loadStart, .loadMoreStart, .noMoreCanLoad: 49 | break 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /MVVM/Base/Protocol/ViewModelMultipleContentDataProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelMultipleContentDataProtocol.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/7. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias StandardMultipleContentViewModel = ViewModelMultipleContentDataProtocol & ViewModelLoadingProtocol & ViewModelLoadingFuncProtocol 12 | 13 | protocol ViewModelMultipleContentDataProtocol: BaseDataProtocol where Model: MultipleContentProtocol { 14 | func numberOfSection() -> Int 15 | func numberOfItem(in section: Int) -> Int 16 | func modelAt(section: Int) -> Model? 17 | func modelAt(indexPath: IndexPath) -> Model.SubModel? 18 | func isLastData(index: IndexPath) -> Bool 19 | } 20 | 21 | extension ViewModelMultipleContentDataProtocol { 22 | 23 | func numberOfSection() -> Int { 24 | return models.count 25 | } 26 | 27 | func numberOfItem(in section: Int) -> Int { 28 | return modelAt(section: section)?.subModels.count ?? 0 29 | } 30 | 31 | func modelAt(section: Int) -> Model? { 32 | return models[safe: section] 33 | } 34 | 35 | func modelAt(indexPath: IndexPath) -> Model.SubModel? { 36 | return models[safe: indexPath.section]?.subModels[safe: indexPath.item] 37 | } 38 | 39 | func isLastData(index: IndexPath) -> Bool { 40 | return index.section + 1 == models.count && index.item + 1 == numberOfItem(in: index.section) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /MVVM/Base/View/EmptyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyView.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EmptyView: UIView { 12 | 13 | let messageLabel = UILabel() 14 | let reloadButton = UIButton() 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | super.init(coder: aDecoder) 18 | setupUI() 19 | } 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | setupUI() 24 | } 25 | 26 | private func setupUI() { 27 | backgroundColor = UIColor.init(netHex: 0xf6f6f6) 28 | addSubview(messageLabel) 29 | addSubview(reloadButton) 30 | 31 | messageLabel.font = UIFont.systemFont(ofSize: 18) 32 | messageLabel.textAlignment = .center 33 | messageLabel.text = "連線異常,請重新嘗試" 34 | messageLabel.numberOfLines = 2 35 | 36 | reloadButton.backgroundColor = UIColor.red 37 | reloadButton.setTitle("重新整理", for: .normal) 38 | reloadButton.titleLabel?.font = UIFont.systemFont(ofSize: 18) 39 | 40 | messageLabel.mLay(.centerX, .equal, self) 41 | messageLabel.mLay(.width, 300) 42 | messageLabel.mLay(.bottom, .equal, self, .centerY, multiplier: 1, constant: -10) 43 | 44 | reloadButton.mLay(.centerX, .equal, self) 45 | reloadButton.mLay(.width, 300) 46 | reloadButton.mLay(.top, .equal, self, .centerY, multiplier: 1, constant: 10) 47 | reloadButton.mLay(.height, 50) 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /MVVM/ViewController/MainVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVC.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MainVC: UIViewController { 12 | 13 | fileprivate lazy var button: UIButton = { 14 | let button = UIButton() 15 | button.backgroundColor = .red 16 | button.setTitle("新北市垃圾車路線", for: .normal) 17 | button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) 18 | return button 19 | }() 20 | 21 | fileprivate lazy var schoolButton: UIButton = { 22 | let button = UIButton() 23 | button.backgroundColor = .red 24 | button.setTitle("學校資料", for: .normal) 25 | button.addTarget(self, action: #selector(didTapSchoolButton), for: .touchUpInside) 26 | return button 27 | }() 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | view.addSubview(button) 32 | button.mLayCenterXY() 33 | button.mLay(size: CGSize(width: 180, height: 50)) 34 | 35 | view.addSubview(schoolButton) 36 | schoolButton.mLay(.centerX, .equal, view) 37 | schoolButton.mLay(size: CGSize(width: 180, height: 50)) 38 | schoolButton.mLay(.centerY, .equal, view, multiplier: 1.5, constant: 0) 39 | } 40 | 41 | @objc func didTapButton() { 42 | self.navigationController?.pushViewController(ZipVC(), animated: true) 43 | } 44 | 45 | @objc func didTapSchoolButton() { 46 | self.navigationController?.pushViewController(SchoolDataVC(), animated: true) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MVVM/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /MVVM/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 | -------------------------------------------------------------------------------- /MVVM/Base/Protocol/JsonProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsonProtocol.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias JsonModel = Codable & JsonProtocol 12 | 13 | protocol MultipleContentProtocol: JsonModel { 14 | associatedtype SubModel: JsonModel 15 | var subModels: [SubModel] {get set} 16 | } 17 | 18 | protocol JsonProtocol { 19 | static func decodeJSON(data: Data?) throws -> Self 20 | func encodeToJSON() throws -> Data 21 | } 22 | 23 | extension JsonProtocol where Self: Codable { 24 | 25 | static func decodeJSON(data: Data?) throws -> Self { 26 | guard let data = data else { throw JSONConvertError.noDataError } 27 | do { 28 | return try JSONDecoder().decode(self, from: data) 29 | } catch { 30 | throw error 31 | } 32 | } 33 | 34 | func encodeToJSON() throws -> Data { 35 | do { 36 | return try JSONEncoder().encode(self) 37 | } catch { 38 | throw error 39 | } 40 | } 41 | } 42 | 43 | extension Data { 44 | func decodeToModel(type: T.Type) throws -> T { 45 | do { 46 | return try T.decodeJSON(data: self) 47 | } catch { 48 | throw error 49 | } 50 | } 51 | 52 | func decodeToModelArray(type: T.Type) throws -> [T] { 53 | do { 54 | return try JSONDecoder().decode([T].self, from: self) 55 | } catch { 56 | throw error 57 | } 58 | } 59 | } 60 | 61 | extension Array where Element: JsonModel { 62 | func encodeToJSON() throws -> Data { 63 | do { 64 | return try JSONEncoder().encode(self) 65 | } catch { 66 | throw error 67 | } 68 | } 69 | } 70 | 71 | extension Data { 72 | 73 | @nonobjc var string: String? { 74 | return String(data: self, encoding: .utf8) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /MVVM/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /MVVM/Base/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum JSONConvertError: Error, LocalizedError, CustomNSError { 12 | case noDataError 13 | 14 | var localizedDescription: String { 15 | return "JSON Convert Error" 16 | } 17 | 18 | public var errorDescription: String? { 19 | switch self { 20 | case .noDataError: return "無輸入資料" 21 | } 22 | } 23 | 24 | public static var errorDomain: String { 25 | return "JSON Convert Error" 26 | } 27 | 28 | public var errorCode: Int { 29 | switch self { 30 | case .noDataError: return 8000 31 | } 32 | } 33 | 34 | public var errorUserInfo: [String: Any] { 35 | switch self { 36 | case .noDataError: return [:] 37 | } 38 | } 39 | } 40 | 41 | enum WebError: Error, LocalizedError, CustomNSError { 42 | static let url = "url" 43 | static let heads = "httpHeads" 44 | static let body = "httpBody" 45 | 46 | case convertFail(userInfo: [String: Any]) 47 | case unKnowError(userInfo: [String: Any]) 48 | 49 | var localizedDescription: String { 50 | switch self { 51 | case .convertFail: return "網路請求錯誤 - 伺服器回傳資料型別錯誤" 52 | case .unKnowError: return "網路請求錯誤 - 未知錯誤" 53 | } 54 | } 55 | 56 | public var errorDescription: String? { 57 | switch self { 58 | case .convertFail: return "網路請求錯誤 - 伺服器回傳資料型別錯誤" 59 | case .unKnowError: return "網路請求錯誤 - 未知錯誤" 60 | } 61 | } 62 | 63 | public static var errorDomain: String { 64 | return "網路請求錯誤" 65 | } 66 | 67 | public var errorCode: Int { 68 | switch self { 69 | case .convertFail: return 9000 70 | case .unKnowError: return 9001 71 | } 72 | } 73 | 74 | public var errorUserInfo: [String: Any] { 75 | switch self { 76 | case .convertFail(let dic): return dic 77 | case .unKnowError(let dic): return dic 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /MVVM/ViewController/GarbageStationInfoVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GarbageStationInfoVC.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GarbageStationInfoVC: BaseVC { 12 | 13 | fileprivate lazy var viewModel: GarbageStationInfoViewModel = GarbageStationInfoViewModel() 14 | 15 | fileprivate let tableView: UITableView = UITableView() 16 | 17 | convenience init(with area: String) { 18 | self.init() 19 | self.viewModel = GarbageStationInfoViewModel(with: area) 20 | self.title = area 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | setupUI() 26 | viewModel.loadingDelegate = self 27 | viewModel.loadingStatusDelegate = self 28 | viewModel.nextStatus() 29 | } 30 | 31 | private func setupUI() { 32 | view.addSubview(tableView) 33 | tableView.mLay(pin: .zero) 34 | tableView.dataSource = self 35 | tableView.delegate = self 36 | tableView.register(GarbageStationInfoCell.self, forCellReuseIdentifier: "cell") 37 | } 38 | 39 | override func didTapReloadButton() { 40 | viewModel.refreshData() 41 | } 42 | } 43 | 44 | extension GarbageStationInfoVC: UITableViewDataSource, UITableViewDelegate { 45 | 46 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return viewModel.datasCount 48 | } 49 | 50 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! GarbageStationInfoCell 52 | if let model = viewModel.model(at: indexPath.row) { 53 | cell.setup(with: model) 54 | } 55 | return cell 56 | } 57 | 58 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 59 | if viewModel.isLastData(index: indexPath.row) { 60 | viewModel.nextStatus() 61 | } 62 | } 63 | } 64 | 65 | extension GarbageStationInfoVC: ViewModelLoadingDelegate { 66 | 67 | func loadingDone() { 68 | tableView.reloadData() 69 | } 70 | 71 | func loadingFail(_ error: Error?) { 72 | 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /MVVM/Base/ViewController/BaseVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseVC.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SVProgressHUD 11 | 12 | class BaseVC: UIViewController { 13 | fileprivate var emptyView: EmptyView? 14 | fileprivate var noDataView: NoDataView? 15 | } 16 | 17 | extension BaseVC: ViewModelLoadingStatusDelegate { 18 | func showLoading(_ bool: Bool) { 19 | 20 | if bool { 21 | DispatchQueue.main.async { 22 | SVProgressHUD.show() 23 | } 24 | } else { 25 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 26 | SVProgressHUD.dismiss() 27 | } 28 | } 29 | } 30 | 31 | func showEmptyView(with error: Error?) { 32 | if error != nil { 33 | if emptyView == nil { 34 | let emptyView = EmptyView() 35 | emptyView.alpha = 0 36 | emptyView.reloadButton.addTarget(self, action: #selector(didTapReloadButton), for: .touchUpInside) 37 | self.emptyView = emptyView 38 | self.view.addSubview(emptyView) 39 | emptyView.mLay(pin: .zero) 40 | self.view.bringSubview(toFront: emptyView) 41 | } 42 | UIView.animate(withDuration: 0.2) { 43 | self.emptyView?.alpha = 1 44 | } 45 | } else { 46 | if noDataView == nil { 47 | let noDataView = NoDataView() 48 | noDataView.alpha = 0 49 | self.noDataView = noDataView 50 | self.view.addSubview(noDataView) 51 | noDataView.mLay(pin: .zero) 52 | self.view.bringSubview(toFront: noDataView) 53 | } 54 | UIView.animate(withDuration: 0.2, animations: { 55 | self.noDataView?.alpha = 1 56 | }) 57 | } 58 | } 59 | 60 | func removeEmptyView() { 61 | if emptyView != nil { 62 | UIView.animate(withDuration: 0.2, animations: { 63 | self.emptyView?.alpha = 0 64 | self.emptyView?.removeFromSuperview() 65 | }) 66 | } 67 | if noDataView != nil { 68 | UIView.animate(withDuration: 0.2, animations: { 69 | self.noDataView?.alpha = 0 70 | self.noDataView?.removeFromSuperview() 71 | }) 72 | } 73 | } 74 | 75 | @objc func didTapReloadButton() { 76 | assert(false, "if sub class need using emptyView, remeber override this method :\(#function)") 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /MVVM/ViewController/ZipVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipVC.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ZipVC: BaseVC { 12 | 13 | fileprivate lazy var collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: self.flowLayout) 14 | fileprivate let flowLayout = UICollectionViewFlowLayout() 15 | fileprivate lazy var viewModel: ZipViewModel = ZipViewModel() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | setupUI() 20 | setupFlowLayout() 21 | viewModel.loadingDelegate = self 22 | viewModel.loadingStatusDelegate = self 23 | viewModel.nextStatus() 24 | } 25 | 26 | private func setupUI() { 27 | view.addSubview(collectionView) 28 | collectionView.mLay(pin: .zero) 29 | collectionView.backgroundColor = .white 30 | collectionView.delegate = self 31 | collectionView.dataSource = self 32 | collectionView.register(ZipCollectionViewCell.self, forCellWithReuseIdentifier: "cell") 33 | } 34 | 35 | private func setupFlowLayout() { 36 | flowLayout.sectionInset = .init(top: 10, left: 10, bottom: 20, right: 10) 37 | flowLayout.minimumLineSpacing = 10 38 | flowLayout.minimumInteritemSpacing = 10 39 | let width = CGFloat(Int((UIScreen.main.bounds.width - 40) / 3)) 40 | flowLayout.itemSize = CGSize(width: width, height: 50) 41 | } 42 | 43 | override func didTapReloadButton() { 44 | viewModel.refreshData() 45 | } 46 | } 47 | 48 | extension ZipVC: UICollectionViewDataSource, UICollectionViewDelegate { 49 | 50 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 51 | return viewModel.datasCount 52 | } 53 | 54 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 55 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ZipCollectionViewCell 56 | if let model = viewModel.model(at: indexPath.item) { 57 | cell.setup(with: model) 58 | } 59 | return cell 60 | } 61 | 62 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 63 | guard let model = viewModel.model(at: indexPath.item) else { return } 64 | let vc = GarbageStationInfoVC(with: model.district) 65 | self.navigationController?.pushViewController(vc, animated: true) 66 | } 67 | } 68 | 69 | extension ZipVC: ViewModelLoadingDelegate { 70 | 71 | func loadingDone() { 72 | collectionView.reloadData() 73 | } 74 | 75 | func loadingFail(_ error: Error?) { 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /MVVM/Services/WebService/ChlURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChlURLProtocol.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ChlURLProtocol: URLProtocol, URLSessionDataDelegate { 12 | 13 | fileprivate var session: URLSession? 14 | fileprivate var sessionTask: URLSessionTask? 15 | 16 | fileprivate var clientThread: Thread? 17 | 18 | override class func canInit(with request: URLRequest) -> Bool { 19 | if URLProtocol.property(forKey: "process", in: request) != nil { 20 | return false 21 | } 22 | return true 23 | } 24 | 25 | override class func canInit(with task: URLSessionTask) -> Bool { 26 | guard let request = task.originalRequest else { 27 | return false 28 | } 29 | 30 | if URLProtocol.property(forKey: "process", in: request) != nil { 31 | return false 32 | } 33 | return true 34 | } 35 | 36 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 37 | 38 | let newRequest = request as! NSMutableURLRequest 39 | URLProtocol.setProperty("process", forKey: "process", in: newRequest) 40 | return newRequest as URLRequest 41 | } 42 | 43 | override func startLoading() { 44 | 45 | clientThread = Thread.current 46 | 47 | session = URLSession(configuration: .default, delegate: self, delegateQueue: URLSession.shared.delegateQueue) 48 | sessionTask = self.session?.dataTask(with: self.request) { [weak self] (data, res, err) in 49 | guard let strongSelf = self else { return } 50 | 51 | strongSelf.threadSafety { 52 | 53 | if let err = err { 54 | strongSelf.client?.urlProtocol(strongSelf, didFailWithError: err) 55 | } else { 56 | strongSelf.client?.urlProtocol(strongSelf, didReceive: res!, cacheStoragePolicy: .allowed) 57 | strongSelf.client?.urlProtocol(strongSelf, didLoad: data!) 58 | strongSelf.client?.urlProtocolDidFinishLoading(strongSelf) 59 | } 60 | } 61 | } 62 | sessionTask?.resume() 63 | } 64 | 65 | override func stopLoading() { 66 | self.sessionTask?.cancel() 67 | self.sessionTask = nil 68 | self.session = nil 69 | } 70 | 71 | func threadSafety(_ clourse: (() -> Void)) { 72 | guard let thread = clientThread else { return } 73 | perform(#selector(performClourseOnThread(_:)), on: thread, with: clourse, waitUntilDone: false) 74 | } 75 | 76 | @objc func performClourseOnThread(_ clourse: Any) { 77 | guard let clourse = clourse as? () -> Void else { return } 78 | clourse() 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /MVVM/ViewController/SchoolDataVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchoolDataVC.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/7. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SchoolDataVC: BaseVC { 12 | 13 | fileprivate lazy var viewModel: SchoolDataViewModel = SchoolDataViewModel() 14 | 15 | fileprivate let tableView: UITableView = UITableView() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | setupUI() 20 | viewModel.loadingDelegate = self 21 | viewModel.loadingStatusDelegate = self 22 | viewModel.nextStatus() 23 | } 24 | 25 | private func setupUI() { 26 | view.addSubview(tableView) 27 | tableView.mLay(pin: .zero) 28 | tableView.dataSource = self 29 | tableView.delegate = self 30 | tableView.register(SchoolDataCell.self, forCellReuseIdentifier: "cell") 31 | } 32 | 33 | override func didTapReloadButton() { 34 | viewModel.refreshData() 35 | } 36 | 37 | } 38 | 39 | extension SchoolDataVC: UITableViewDataSource, UITableViewDelegate { 40 | 41 | func numberOfSections(in tableView: UITableView) -> Int { 42 | return viewModel.numberOfSection() 43 | } 44 | 45 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 | return viewModel.numberOfItem(in: section) 47 | } 48 | 49 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 50 | if let model = viewModel.modelAt(section: section) { 51 | let headerLabel = UILabel() 52 | headerLabel.text = " 🔷班級: \(model.className), 導師: \(model.tutorName)" 53 | headerLabel.backgroundColor = .red 54 | return headerLabel 55 | } 56 | return nil 57 | } 58 | 59 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 60 | return viewModel.modelAt(section: section) != nil ? 50 : 0 61 | } 62 | 63 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 64 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! SchoolDataCell 65 | if let model = viewModel.modelAt(indexPath: indexPath) { 66 | cell.setup(with: model) 67 | } 68 | return cell 69 | } 70 | 71 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 72 | if viewModel.isLastData(index: indexPath) { 73 | viewModel.nextStatus() 74 | } 75 | } 76 | } 77 | 78 | extension SchoolDataVC: ViewModelLoadingDelegate { 79 | 80 | func loadingDone() { 81 | tableView.reloadData() 82 | } 83 | 84 | func loadingFail(_ error: Error?) { 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /MVVM/Services/WebService/WebService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebService.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | typealias DataCompleClosure = (Data) -> Void 13 | typealias LoadDataFailClosure = (Error?) -> Void 14 | 15 | final class WebService: SessionManager { 16 | 17 | static let shared = WebService() 18 | private convenience init() { 19 | let config = URLSessionConfiguration.default 20 | config.protocolClasses = [ChlURLProtocol.self] 21 | config.timeoutIntervalForRequest = 10 22 | self.init(configuration: config, delegate: SessionDelegate(), serverTrustPolicyManager: nil) 23 | } 24 | 25 | fileprivate func request(method: HTTPMethod, url: String = String(), parameters: [String: Any]? = nil, handle: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) { 26 | request(url, method: method, parameters: parameters).validate().response { response in 27 | handle(response.data, response.response, response.error) 28 | } 29 | } 30 | 31 | fileprivate func baseRequest(method: HTTPMethod, url: String = String(), parameters: [String: Any]?, success: DataCompleClosure?, fail: LoadDataFailClosure?) { 32 | request(method: .get, url: url, parameters: parameters) { (data, res, error) in 33 | 34 | guard error == nil else { 35 | fail?(error ?? WebError.unKnowError(userInfo: [WebError.url: url, WebError.heads: res?.allHeaderFields ?? [AnyHashable: Any](), WebError.body: data?.string ?? Data()])) 36 | return 37 | } 38 | guard let datas = data else { 39 | fail?(WebError.convertFail(userInfo: [WebError.url: url, WebError.heads: res?.allHeaderFields ?? [AnyHashable: Any](), WebError.body: data ?? Data()])) 40 | return 41 | } 42 | success?(datas) 43 | return 44 | } 45 | } 46 | 47 | } 48 | 49 | extension WebService { 50 | 51 | func getZipData(success: DataCompleClosure?, fail: LoadDataFailClosure?) { 52 | let url = "http://data.ntpc.gov.tw/od/data/api/AC110AF8-C847-43E5-B62C-7985E9E049F9" 53 | var para: [String: Any] = [:] 54 | para["$format"] = "json" 55 | baseRequest(method: .get, url: url, parameters: para, success: success, fail: fail) 56 | } 57 | 58 | func getGarbageStationInfo(with area: String, skip: Int, success: DataCompleClosure?, fail: LoadDataFailClosure?) { 59 | let path = "http://data.ntpc.gov.tw/od/data/api/EDC3AD26-8AE7-4916-A00B-BC6048D19BF8" 60 | var para: [String: Any] = [:] 61 | para["$format"] = "json" 62 | para["$filter"] = "city eq \(area)" 63 | para["$skip"] = skip 64 | para["$top"] = 40 65 | baseRequest(method: .get, url: path, parameters: para, success: success, fail: fail) 66 | } 67 | 68 | func getSchoolData(success: DataCompleClosure?, fail: LoadDataFailClosure?) { 69 | let url = "https://github.com/nick6969/MVVM/blob/master/MVVM/data.json?raw=true" 70 | baseRequest(method: .get, url: url, parameters: nil, success: success, fail: fail) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MVVM/Base/ViewModel/BaseViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewModel.swift 3 | // MVVM 4 | // 5 | // Created by Nick Lin on 2018/4/3. 6 | // Copyright © 2018年 Nick Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BaseViewModel where T: JsonModel { 12 | weak var loadingDelegate: ViewModelLoadingDelegate? 13 | weak var loadingStatusDelegate: ViewModelLoadingStatusDelegate? 14 | var status: ViewModelStatus = .initialize 15 | 16 | var models: [T] = [] { 17 | didSet { 18 | if models.isEmpty && status != .loadStart { 19 | DispatchQueue.main.async { 20 | self.loadingStatusDelegate?.showEmptyView(with: nil) 21 | } 22 | } 23 | } 24 | } 25 | 26 | var modelsSuccessClosure: (([T]) -> Void)? 27 | var modelSuccessClosure: ((T) -> Void)? 28 | var loadingFailClosure: ( (Error?) -> Void )? 29 | 30 | var dataConvertToModelsClosure: ((Data) -> Void)? 31 | var dataConvertToModelClosure: ((Data) -> Void)? 32 | 33 | init() { 34 | setupClosure() 35 | } 36 | 37 | fileprivate func setupClosure() { 38 | 39 | dataConvertToModelsClosure = { [weak self] data in 40 | guard let `self` = self else { return } 41 | do { 42 | let models = try data.decodeToModelArray(type: T.self) 43 | self.modelsSuccessClosure?(models) 44 | } catch { 45 | self.loadingFailClosure?(error) 46 | } 47 | } 48 | 49 | dataConvertToModelClosure = { [weak self] data in 50 | guard let `self` = self else { return } 51 | do { 52 | let model = try data.decodeToModel(type: T.self) 53 | self.modelSuccessClosure?(model) 54 | } catch { 55 | self.loadingFailClosure?(error) 56 | } 57 | } 58 | 59 | modelsSuccessClosure = { [weak self] models in 60 | guard let `self` = self else { return } 61 | if self.status == .loadStart { 62 | self.models = models 63 | } else if self.status == .loadMoreStart { 64 | if models.isEmpty { 65 | self.status = .noMoreCanLoad 66 | } else { 67 | self.models.append(contentsOf: models) 68 | } 69 | } 70 | self.loadDoneChangeStatus() 71 | DispatchQueue.main.async { 72 | self.loadingStatusDelegate?.showLoading(false) 73 | if self.models.isEmpty { 74 | self.loadingStatusDelegate?.showEmptyView(with: nil) 75 | } else { 76 | self.loadingStatusDelegate?.removeEmptyView() 77 | } 78 | self.loadingDelegate?.loadingDone() 79 | } 80 | } 81 | 82 | modelSuccessClosure = { [weak self] model in 83 | guard let `self` = self else { return } 84 | self.models = [model] 85 | self.loadDoneChangeStatus() 86 | DispatchQueue.main.async { 87 | self.loadingStatusDelegate?.showLoading(false) 88 | self.loadingStatusDelegate?.removeEmptyView() 89 | self.loadingDelegate?.loadingDone() 90 | } 91 | } 92 | 93 | loadingFailClosure = { [weak self] error in 94 | guard let `self` = self else { return } 95 | self.loadFailChangeStatus() 96 | DispatchQueue.main.async { 97 | self.loadingStatusDelegate?.showLoading(false) 98 | if self.models.isEmpty { 99 | self.loadingStatusDelegate?.showEmptyView(with: error) 100 | } 101 | self.loadingDelegate?.loadingFail(error) 102 | } 103 | } 104 | } 105 | 106 | fileprivate func loadDoneChangeStatus() { 107 | switch status { 108 | case .loadStart: 109 | status = .loadDone 110 | case .loadMoreStart: 111 | status = .loadMoreDone 112 | default: 113 | break 114 | } 115 | } 116 | 117 | fileprivate func loadFailChangeStatus() { 118 | switch status { 119 | case .loadStart: 120 | status = .loadFail 121 | case .loadMoreStart: 122 | status = .loadMoreFail 123 | default: 124 | break 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /sortXcodeProject.pl: -------------------------------------------------------------------------------- 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 Computer, 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 | } -------------------------------------------------------------------------------- /MVVM.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 01919CBE2073C36A00400B51 /* ChlURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01919CBD2073C36A00400B51 /* ChlURLProtocol.swift */; }; 11 | 01919CC02073C47300400B51 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01919CBF2073C47300400B51 /* Tools.swift */; }; 12 | 0194DAAF2078081F000328AF /* ViewModelMultipleContentDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194DAAE2078081F000328AF /* ViewModelMultipleContentDataProtocol.swift */; }; 13 | 0194DAB12078089B000328AF /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194DAB02078089B000328AF /* ArrayExtension.swift */; }; 14 | 0194DAB320780D5F000328AF /* ClassModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194DAB220780D5F000328AF /* ClassModel.swift */; }; 15 | 0194DAB720780FE2000328AF /* SchoolDataViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194DAB620780FE2000328AF /* SchoolDataViewModel.swift */; }; 16 | 0194DABB207817FE000328AF /* SchoolDataVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194DABA207817FE000328AF /* SchoolDataVC.swift */; }; 17 | 0194DABD20781984000328AF /* SchoolDataCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194DABC20781984000328AF /* SchoolDataCell.swift */; }; 18 | 0194DAC120782241000328AF /* data.json in Resources */ = {isa = PBXBuildFile; fileRef = 0194DAC020782241000328AF /* data.json */; }; 19 | 01A279E720735B6F0045DE1C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A279E620735B6F0045DE1C /* AppDelegate.swift */; }; 20 | 01A279EE20735B6F0045DE1C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01A279ED20735B6F0045DE1C /* Assets.xcassets */; }; 21 | 01A279F120735B6F0045DE1C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 01A279EF20735B6F0045DE1C /* LaunchScreen.storyboard */; }; 22 | 01CFAB6520735C8900720BF6 /* JsonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB6420735C8900720BF6 /* JsonProtocol.swift */; }; 23 | 01CFAB6720735CE700720BF6 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB6620735CE700720BF6 /* Error.swift */; }; 24 | 01CFAB6E20735E0100720BF6 /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB6D20735E0100720BF6 /* ViewModelProtocol.swift */; }; 25 | 01CFAB7020735E9500720BF6 /* ViewModelStatusEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB6F20735E9500720BF6 /* ViewModelStatusEnum.swift */; }; 26 | 01CFAB7220735EF400720BF6 /* ViewModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB7120735EF400720BF6 /* ViewModelDelegate.swift */; }; 27 | 01CFAB7420735FD300720BF6 /* BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB7320735FD300720BF6 /* BaseViewModel.swift */; }; 28 | 01CFAB792073645200720BF6 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB782073645200720BF6 /* BaseVC.swift */; }; 29 | 01CFAB7C2073665800720BF6 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB7B2073665800720BF6 /* EmptyView.swift */; }; 30 | 01CFAB7E207366B600720BF6 /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB7D207366B600720BF6 /* UIColorExtension.swift */; }; 31 | 01CFAB802073671200720BF6 /* NoDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB7F2073671200720BF6 /* NoDataView.swift */; }; 32 | 01CFAB852073695700720BF6 /* WebService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB842073695700720BF6 /* WebService.swift */; }; 33 | 01CFAB8820736F6400720BF6 /* ZipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB8720736F6400720BF6 /* ZipModel.swift */; }; 34 | 01CFAB8A2073702A00720BF6 /* GarbageStationInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB892073702A00720BF6 /* GarbageStationInfoModel.swift */; }; 35 | 01CFAB8D2073719000720BF6 /* ZipViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB8C2073719000720BF6 /* ZipViewModel.swift */; }; 36 | 01CFAB8F2073720900720BF6 /* GarbageStationInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB8E2073720900720BF6 /* GarbageStationInfoViewModel.swift */; }; 37 | 01CFAB95207372B600720BF6 /* MainVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB94207372B600720BF6 /* MainVC.swift */; }; 38 | 01CFAB97207373C200720BF6 /* ZipVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB96207373C200720BF6 /* ZipVC.swift */; }; 39 | 01CFAB992073770A00720BF6 /* ZipCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB982073770A00720BF6 /* ZipCollectionViewCell.swift */; }; 40 | 01CFAB9B20737C5D00720BF6 /* GarbageStationInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB9A20737C5D00720BF6 /* GarbageStationInfoVC.swift */; }; 41 | 01CFAB9D20737D9E00720BF6 /* GarbageStationInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CFAB9C20737D9E00720BF6 /* GarbageStationInfoCell.swift */; }; 42 | 01F7D5F8207CDA07006CB1FB /* BaseDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F7D5F7207CDA07006CB1FB /* BaseDataProtocol.swift */; }; 43 | F8BEC2ED52579ED6A147E636 /* Pods_MVVM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5D796F75F668E558ECF32D /* Pods_MVVM.framework */; }; 44 | /* End PBXBuildFile section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 01919CBD2073C36A00400B51 /* ChlURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChlURLProtocol.swift; sourceTree = ""; }; 48 | 01919CBF2073C47300400B51 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; 49 | 0194DAAE2078081F000328AF /* ViewModelMultipleContentDataProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelMultipleContentDataProtocol.swift; sourceTree = ""; }; 50 | 0194DAB02078089B000328AF /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; 51 | 0194DAB220780D5F000328AF /* ClassModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassModel.swift; sourceTree = ""; }; 52 | 0194DAB620780FE2000328AF /* SchoolDataViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchoolDataViewModel.swift; sourceTree = ""; }; 53 | 0194DABA207817FE000328AF /* SchoolDataVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchoolDataVC.swift; sourceTree = ""; }; 54 | 0194DABC20781984000328AF /* SchoolDataCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchoolDataCell.swift; sourceTree = ""; }; 55 | 0194DAC020782241000328AF /* data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = data.json; sourceTree = ""; }; 56 | 01A279E320735B6F0045DE1C /* MVVM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MVVM.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 01A279E620735B6F0045DE1C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 58 | 01A279ED20735B6F0045DE1C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 59 | 01A279F020735B6F0045DE1C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 60 | 01A279F220735B6F0045DE1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 01CFAB6420735C8900720BF6 /* JsonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonProtocol.swift; sourceTree = ""; }; 62 | 01CFAB6620735CE700720BF6 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 63 | 01CFAB6D20735E0100720BF6 /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; }; 64 | 01CFAB6F20735E9500720BF6 /* ViewModelStatusEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelStatusEnum.swift; sourceTree = ""; }; 65 | 01CFAB7120735EF400720BF6 /* ViewModelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelDelegate.swift; sourceTree = ""; }; 66 | 01CFAB7320735FD300720BF6 /* BaseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewModel.swift; sourceTree = ""; }; 67 | 01CFAB782073645200720BF6 /* BaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseVC.swift; sourceTree = ""; }; 68 | 01CFAB7B2073665800720BF6 /* EmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; }; 69 | 01CFAB7D207366B600720BF6 /* UIColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; }; 70 | 01CFAB7F2073671200720BF6 /* NoDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoDataView.swift; sourceTree = ""; }; 71 | 01CFAB842073695700720BF6 /* WebService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebService.swift; sourceTree = ""; }; 72 | 01CFAB8720736F6400720BF6 /* ZipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipModel.swift; sourceTree = ""; }; 73 | 01CFAB892073702A00720BF6 /* GarbageStationInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageStationInfoModel.swift; sourceTree = ""; }; 74 | 01CFAB8C2073719000720BF6 /* ZipViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipViewModel.swift; sourceTree = ""; }; 75 | 01CFAB8E2073720900720BF6 /* GarbageStationInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageStationInfoViewModel.swift; sourceTree = ""; }; 76 | 01CFAB94207372B600720BF6 /* MainVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainVC.swift; sourceTree = ""; }; 77 | 01CFAB96207373C200720BF6 /* ZipVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipVC.swift; sourceTree = ""; }; 78 | 01CFAB982073770A00720BF6 /* ZipCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipCollectionViewCell.swift; sourceTree = ""; }; 79 | 01CFAB9A20737C5D00720BF6 /* GarbageStationInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageStationInfoVC.swift; sourceTree = ""; }; 80 | 01CFAB9C20737D9E00720BF6 /* GarbageStationInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageStationInfoCell.swift; sourceTree = ""; }; 81 | 01F7D5F7207CDA07006CB1FB /* BaseDataProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDataProtocol.swift; sourceTree = ""; }; 82 | 41924B0A77A02B66C1CE8545 /* Pods-MVVM.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MVVM.release.xcconfig"; path = "Pods/Target Support Files/Pods-MVVM/Pods-MVVM.release.xcconfig"; sourceTree = ""; }; 83 | DA5D796F75F668E558ECF32D /* Pods_MVVM.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MVVM.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 84 | FF129F2F693F8BE2354CB5E2 /* Pods-MVVM.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MVVM.debug.xcconfig"; path = "Pods/Target Support Files/Pods-MVVM/Pods-MVVM.debug.xcconfig"; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | 01A279E020735B6F0045DE1C /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | F8BEC2ED52579ED6A147E636 /* Pods_MVVM.framework in Frameworks */, 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | 01A279DA20735B6F0045DE1C = { 100 | isa = PBXGroup; 101 | children = ( 102 | 6420965D9D701EBD4C87971F /* Frameworks */, 103 | 01A279E520735B6F0045DE1C /* MVVM */, 104 | 30374CBEA4692B7246227482 /* Pods */, 105 | 01A279E420735B6F0045DE1C /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | 01A279E420735B6F0045DE1C /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 01A279E320735B6F0045DE1C /* MVVM.app */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | 01A279E520735B6F0045DE1C /* MVVM */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 01CFAB6A20735D6800720BF6 /* Base */, 121 | 01CFAB6320735C7800720BF6 /* Extension */, 122 | 01CFAB82207368FB00720BF6 /* Services */, 123 | 01CFAB6220735C3F00720BF6 /* Support */, 124 | 01CFAB902073727400720BF6 /* View */, 125 | 01CFAB8620736F5900720BF6 /* Model */, 126 | 01CFAB93207372A900720BF6 /* ViewController */, 127 | 01CFAB8B2073717F00720BF6 /* ViewModel */, 128 | 01A279E620735B6F0045DE1C /* AppDelegate.swift */, 129 | ); 130 | path = MVVM; 131 | sourceTree = ""; 132 | }; 133 | 01CFAB6220735C3F00720BF6 /* Support */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 0194DAC020782241000328AF /* data.json */, 137 | 01A279ED20735B6F0045DE1C /* Assets.xcassets */, 138 | 01A279F220735B6F0045DE1C /* Info.plist */, 139 | 01A279EF20735B6F0045DE1C /* LaunchScreen.storyboard */, 140 | ); 141 | name = Support; 142 | sourceTree = ""; 143 | }; 144 | 01CFAB6320735C7800720BF6 /* Extension */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 01CFAB7D207366B600720BF6 /* UIColorExtension.swift */, 148 | 01919CBF2073C47300400B51 /* Tools.swift */, 149 | 0194DAB02078089B000328AF /* ArrayExtension.swift */, 150 | ); 151 | path = Extension; 152 | sourceTree = ""; 153 | }; 154 | 01CFAB6A20735D6800720BF6 /* Base */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 01CFAB6C20735D7B00720BF6 /* ViewModel */, 158 | 01CFAB6B20735D7300720BF6 /* Protocol */, 159 | 01CFAB7A2073664C00720BF6 /* View */, 160 | 01CFAB772073643800720BF6 /* ViewController */, 161 | 01CFAB6620735CE700720BF6 /* Error.swift */, 162 | ); 163 | path = Base; 164 | sourceTree = ""; 165 | }; 166 | 01CFAB6B20735D7300720BF6 /* Protocol */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 01CFAB6420735C8900720BF6 /* JsonProtocol.swift */, 170 | 01CFAB7120735EF400720BF6 /* ViewModelDelegate.swift */, 171 | 01CFAB6D20735E0100720BF6 /* ViewModelProtocol.swift */, 172 | 01CFAB6F20735E9500720BF6 /* ViewModelStatusEnum.swift */, 173 | 0194DAAE2078081F000328AF /* ViewModelMultipleContentDataProtocol.swift */, 174 | 01F7D5F7207CDA07006CB1FB /* BaseDataProtocol.swift */, 175 | ); 176 | path = Protocol; 177 | sourceTree = ""; 178 | }; 179 | 01CFAB6C20735D7B00720BF6 /* ViewModel */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 01CFAB7320735FD300720BF6 /* BaseViewModel.swift */, 183 | ); 184 | path = ViewModel; 185 | sourceTree = ""; 186 | }; 187 | 01CFAB772073643800720BF6 /* ViewController */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | 01CFAB782073645200720BF6 /* BaseVC.swift */, 191 | ); 192 | path = ViewController; 193 | sourceTree = ""; 194 | }; 195 | 01CFAB7A2073664C00720BF6 /* View */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 01CFAB7B2073665800720BF6 /* EmptyView.swift */, 199 | 01CFAB7F2073671200720BF6 /* NoDataView.swift */, 200 | ); 201 | path = View; 202 | sourceTree = ""; 203 | }; 204 | 01CFAB82207368FB00720BF6 /* Services */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 01CFAB832073694800720BF6 /* WebService */, 208 | ); 209 | path = Services; 210 | sourceTree = ""; 211 | }; 212 | 01CFAB832073694800720BF6 /* WebService */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 01919CBD2073C36A00400B51 /* ChlURLProtocol.swift */, 216 | 01CFAB842073695700720BF6 /* WebService.swift */, 217 | ); 218 | path = WebService; 219 | sourceTree = ""; 220 | }; 221 | 01CFAB8620736F5900720BF6 /* Model */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 01CFAB892073702A00720BF6 /* GarbageStationInfoModel.swift */, 225 | 01CFAB8720736F6400720BF6 /* ZipModel.swift */, 226 | 0194DAB220780D5F000328AF /* ClassModel.swift */, 227 | ); 228 | path = Model; 229 | sourceTree = ""; 230 | }; 231 | 01CFAB8B2073717F00720BF6 /* ViewModel */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 01CFAB8E2073720900720BF6 /* GarbageStationInfoViewModel.swift */, 235 | 01CFAB8C2073719000720BF6 /* ZipViewModel.swift */, 236 | 0194DAB620780FE2000328AF /* SchoolDataViewModel.swift */, 237 | ); 238 | path = ViewModel; 239 | sourceTree = ""; 240 | }; 241 | 01CFAB902073727400720BF6 /* View */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | 01CFAB912073727D00720BF6 /* CollectionViewCell */, 245 | 01CFAB922073728600720BF6 /* TableViewCell */, 246 | ); 247 | path = View; 248 | sourceTree = ""; 249 | }; 250 | 01CFAB912073727D00720BF6 /* CollectionViewCell */ = { 251 | isa = PBXGroup; 252 | children = ( 253 | 01CFAB982073770A00720BF6 /* ZipCollectionViewCell.swift */, 254 | ); 255 | path = CollectionViewCell; 256 | sourceTree = ""; 257 | }; 258 | 01CFAB922073728600720BF6 /* TableViewCell */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | 01CFAB9C20737D9E00720BF6 /* GarbageStationInfoCell.swift */, 262 | 0194DABC20781984000328AF /* SchoolDataCell.swift */, 263 | ); 264 | path = TableViewCell; 265 | sourceTree = ""; 266 | }; 267 | 01CFAB93207372A900720BF6 /* ViewController */ = { 268 | isa = PBXGroup; 269 | children = ( 270 | 01CFAB9A20737C5D00720BF6 /* GarbageStationInfoVC.swift */, 271 | 01CFAB94207372B600720BF6 /* MainVC.swift */, 272 | 01CFAB96207373C200720BF6 /* ZipVC.swift */, 273 | 0194DABA207817FE000328AF /* SchoolDataVC.swift */, 274 | ); 275 | path = ViewController; 276 | sourceTree = ""; 277 | }; 278 | 30374CBEA4692B7246227482 /* Pods */ = { 279 | isa = PBXGroup; 280 | children = ( 281 | FF129F2F693F8BE2354CB5E2 /* Pods-MVVM.debug.xcconfig */, 282 | 41924B0A77A02B66C1CE8545 /* Pods-MVVM.release.xcconfig */, 283 | ); 284 | name = Pods; 285 | sourceTree = ""; 286 | }; 287 | 6420965D9D701EBD4C87971F /* Frameworks */ = { 288 | isa = PBXGroup; 289 | children = ( 290 | DA5D796F75F668E558ECF32D /* Pods_MVVM.framework */, 291 | ); 292 | name = Frameworks; 293 | sourceTree = ""; 294 | }; 295 | /* End PBXGroup section */ 296 | 297 | /* Begin PBXNativeTarget section */ 298 | 01A279E220735B6F0045DE1C /* MVVM */ = { 299 | isa = PBXNativeTarget; 300 | buildConfigurationList = 01A279F520735B6F0045DE1C /* Build configuration list for PBXNativeTarget "MVVM" */; 301 | buildPhases = ( 302 | AEF328AFE8BB45C64F3AE66E /* [CP] Check Pods Manifest.lock */, 303 | 01A279DF20735B6F0045DE1C /* Sources */, 304 | 01A279E020735B6F0045DE1C /* Frameworks */, 305 | 01A279E120735B6F0045DE1C /* Resources */, 306 | 3EC900E9EE760ECD851E69AB /* [CP] Embed Pods Frameworks */, 307 | 9540B610D2FA978EA5A493AA /* [CP] Copy Pods Resources */, 308 | 01CFAB81207367CC00720BF6 /* Run SwiftLint */, 309 | ); 310 | buildRules = ( 311 | ); 312 | dependencies = ( 313 | ); 314 | name = MVVM; 315 | productName = MVVM; 316 | productReference = 01A279E320735B6F0045DE1C /* MVVM.app */; 317 | productType = "com.apple.product-type.application"; 318 | }; 319 | /* End PBXNativeTarget section */ 320 | 321 | /* Begin PBXProject section */ 322 | 01A279DB20735B6F0045DE1C /* Project object */ = { 323 | isa = PBXProject; 324 | attributes = { 325 | LastSwiftUpdateCheck = 0920; 326 | LastUpgradeCheck = 0920; 327 | ORGANIZATIONNAME = "Nick Lin"; 328 | TargetAttributes = { 329 | 01A279E220735B6F0045DE1C = { 330 | CreatedOnToolsVersion = 9.2; 331 | ProvisioningStyle = Automatic; 332 | }; 333 | }; 334 | }; 335 | buildConfigurationList = 01A279DE20735B6F0045DE1C /* Build configuration list for PBXProject "MVVM" */; 336 | compatibilityVersion = "Xcode 8.0"; 337 | developmentRegion = en; 338 | hasScannedForEncodings = 0; 339 | knownRegions = ( 340 | en, 341 | Base, 342 | ); 343 | mainGroup = 01A279DA20735B6F0045DE1C; 344 | productRefGroup = 01A279E420735B6F0045DE1C /* Products */; 345 | projectDirPath = ""; 346 | projectRoot = ""; 347 | targets = ( 348 | 01A279E220735B6F0045DE1C /* MVVM */, 349 | ); 350 | }; 351 | /* End PBXProject section */ 352 | 353 | /* Begin PBXResourcesBuildPhase section */ 354 | 01A279E120735B6F0045DE1C /* Resources */ = { 355 | isa = PBXResourcesBuildPhase; 356 | buildActionMask = 2147483647; 357 | files = ( 358 | 01A279EE20735B6F0045DE1C /* Assets.xcassets in Resources */, 359 | 0194DAC120782241000328AF /* data.json in Resources */, 360 | 01A279F120735B6F0045DE1C /* LaunchScreen.storyboard in Resources */, 361 | ); 362 | runOnlyForDeploymentPostprocessing = 0; 363 | }; 364 | /* End PBXResourcesBuildPhase section */ 365 | 366 | /* Begin PBXShellScriptBuildPhase section */ 367 | 01CFAB81207367CC00720BF6 /* Run SwiftLint */ = { 368 | isa = PBXShellScriptBuildPhase; 369 | buildActionMask = 2147483647; 370 | files = ( 371 | ); 372 | inputPaths = ( 373 | ); 374 | name = "Run SwiftLint"; 375 | outputPaths = ( 376 | ); 377 | runOnlyForDeploymentPostprocessing = 0; 378 | shellPath = /bin/sh; 379 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; 380 | }; 381 | 3EC900E9EE760ECD851E69AB /* [CP] Embed Pods Frameworks */ = { 382 | isa = PBXShellScriptBuildPhase; 383 | buildActionMask = 2147483647; 384 | files = ( 385 | ); 386 | inputPaths = ( 387 | "${SRCROOT}/Pods/Target Support Files/Pods-MVVM/Pods-MVVM-frameworks.sh", 388 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 389 | "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", 390 | "${BUILT_PRODUCTS_DIR}/mLayout/mLayout.framework", 391 | ); 392 | name = "[CP] Embed Pods Frameworks"; 393 | outputPaths = ( 394 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 395 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", 396 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mLayout.framework", 397 | ); 398 | runOnlyForDeploymentPostprocessing = 0; 399 | shellPath = /bin/sh; 400 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MVVM/Pods-MVVM-frameworks.sh\"\n"; 401 | showEnvVarsInLog = 0; 402 | }; 403 | 9540B610D2FA978EA5A493AA /* [CP] Copy Pods Resources */ = { 404 | isa = PBXShellScriptBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | ); 408 | inputPaths = ( 409 | ); 410 | name = "[CP] Copy Pods Resources"; 411 | outputPaths = ( 412 | ); 413 | runOnlyForDeploymentPostprocessing = 0; 414 | shellPath = /bin/sh; 415 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MVVM/Pods-MVVM-resources.sh\"\n"; 416 | showEnvVarsInLog = 0; 417 | }; 418 | AEF328AFE8BB45C64F3AE66E /* [CP] Check Pods Manifest.lock */ = { 419 | isa = PBXShellScriptBuildPhase; 420 | buildActionMask = 2147483647; 421 | files = ( 422 | ); 423 | inputPaths = ( 424 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 425 | "${PODS_ROOT}/Manifest.lock", 426 | ); 427 | name = "[CP] Check Pods Manifest.lock"; 428 | outputPaths = ( 429 | "$(DERIVED_FILE_DIR)/Pods-MVVM-checkManifestLockResult.txt", 430 | ); 431 | runOnlyForDeploymentPostprocessing = 0; 432 | shellPath = /bin/sh; 433 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 434 | showEnvVarsInLog = 0; 435 | }; 436 | /* End PBXShellScriptBuildPhase section */ 437 | 438 | /* Begin PBXSourcesBuildPhase section */ 439 | 01A279DF20735B6F0045DE1C /* Sources */ = { 440 | isa = PBXSourcesBuildPhase; 441 | buildActionMask = 2147483647; 442 | files = ( 443 | 0194DAB12078089B000328AF /* ArrayExtension.swift in Sources */, 444 | 01A279E720735B6F0045DE1C /* AppDelegate.swift in Sources */, 445 | 01CFAB7420735FD300720BF6 /* BaseViewModel.swift in Sources */, 446 | 01CFAB792073645200720BF6 /* BaseVC.swift in Sources */, 447 | 01919CBE2073C36A00400B51 /* ChlURLProtocol.swift in Sources */, 448 | 0194DAAF2078081F000328AF /* ViewModelMultipleContentDataProtocol.swift in Sources */, 449 | 0194DAB720780FE2000328AF /* SchoolDataViewModel.swift in Sources */, 450 | 01CFAB7C2073665800720BF6 /* EmptyView.swift in Sources */, 451 | 01CFAB6720735CE700720BF6 /* Error.swift in Sources */, 452 | 01919CC02073C47300400B51 /* Tools.swift in Sources */, 453 | 01CFAB9D20737D9E00720BF6 /* GarbageStationInfoCell.swift in Sources */, 454 | 0194DABD20781984000328AF /* SchoolDataCell.swift in Sources */, 455 | 01CFAB8A2073702A00720BF6 /* GarbageStationInfoModel.swift in Sources */, 456 | 01CFAB9B20737C5D00720BF6 /* GarbageStationInfoVC.swift in Sources */, 457 | 01CFAB8F2073720900720BF6 /* GarbageStationInfoViewModel.swift in Sources */, 458 | 01CFAB6520735C8900720BF6 /* JsonProtocol.swift in Sources */, 459 | 01CFAB95207372B600720BF6 /* MainVC.swift in Sources */, 460 | 01CFAB802073671200720BF6 /* NoDataView.swift in Sources */, 461 | 01CFAB7E207366B600720BF6 /* UIColorExtension.swift in Sources */, 462 | 01CFAB7220735EF400720BF6 /* ViewModelDelegate.swift in Sources */, 463 | 01CFAB6E20735E0100720BF6 /* ViewModelProtocol.swift in Sources */, 464 | 01CFAB7020735E9500720BF6 /* ViewModelStatusEnum.swift in Sources */, 465 | 01CFAB852073695700720BF6 /* WebService.swift in Sources */, 466 | 01CFAB992073770A00720BF6 /* ZipCollectionViewCell.swift in Sources */, 467 | 01CFAB8820736F6400720BF6 /* ZipModel.swift in Sources */, 468 | 01CFAB97207373C200720BF6 /* ZipVC.swift in Sources */, 469 | 01F7D5F8207CDA07006CB1FB /* BaseDataProtocol.swift in Sources */, 470 | 0194DABB207817FE000328AF /* SchoolDataVC.swift in Sources */, 471 | 0194DAB320780D5F000328AF /* ClassModel.swift in Sources */, 472 | 01CFAB8D2073719000720BF6 /* ZipViewModel.swift in Sources */, 473 | ); 474 | runOnlyForDeploymentPostprocessing = 0; 475 | }; 476 | /* End PBXSourcesBuildPhase section */ 477 | 478 | /* Begin PBXVariantGroup section */ 479 | 01A279EF20735B6F0045DE1C /* LaunchScreen.storyboard */ = { 480 | isa = PBXVariantGroup; 481 | children = ( 482 | 01A279F020735B6F0045DE1C /* Base */, 483 | ); 484 | name = LaunchScreen.storyboard; 485 | sourceTree = ""; 486 | }; 487 | /* End PBXVariantGroup section */ 488 | 489 | /* Begin XCBuildConfiguration section */ 490 | 01A279F320735B6F0045DE1C /* Debug */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | ALWAYS_SEARCH_USER_PATHS = NO; 494 | CLANG_ANALYZER_NONNULL = YES; 495 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 496 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 497 | CLANG_CXX_LIBRARY = "libc++"; 498 | CLANG_ENABLE_MODULES = YES; 499 | CLANG_ENABLE_OBJC_ARC = YES; 500 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 501 | CLANG_WARN_BOOL_CONVERSION = YES; 502 | CLANG_WARN_COMMA = YES; 503 | CLANG_WARN_CONSTANT_CONVERSION = YES; 504 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 505 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 506 | CLANG_WARN_EMPTY_BODY = YES; 507 | CLANG_WARN_ENUM_CONVERSION = YES; 508 | CLANG_WARN_INFINITE_RECURSION = YES; 509 | CLANG_WARN_INT_CONVERSION = YES; 510 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 511 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 512 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 513 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 514 | CLANG_WARN_STRICT_PROTOTYPES = YES; 515 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 516 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 517 | CLANG_WARN_UNREACHABLE_CODE = YES; 518 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 519 | CODE_SIGN_IDENTITY = "iPhone Developer"; 520 | COPY_PHASE_STRIP = NO; 521 | DEBUG_INFORMATION_FORMAT = dwarf; 522 | ENABLE_STRICT_OBJC_MSGSEND = YES; 523 | ENABLE_TESTABILITY = YES; 524 | GCC_C_LANGUAGE_STANDARD = gnu11; 525 | GCC_DYNAMIC_NO_PIC = NO; 526 | GCC_NO_COMMON_BLOCKS = YES; 527 | GCC_OPTIMIZATION_LEVEL = 0; 528 | GCC_PREPROCESSOR_DEFINITIONS = ( 529 | "DEBUG=1", 530 | "$(inherited)", 531 | ); 532 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 533 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 534 | GCC_WARN_UNDECLARED_SELECTOR = YES; 535 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 536 | GCC_WARN_UNUSED_FUNCTION = YES; 537 | GCC_WARN_UNUSED_VARIABLE = YES; 538 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 539 | MTL_ENABLE_DEBUG_INFO = YES; 540 | ONLY_ACTIVE_ARCH = YES; 541 | SDKROOT = iphoneos; 542 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 543 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 544 | }; 545 | name = Debug; 546 | }; 547 | 01A279F420735B6F0045DE1C /* Release */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | ALWAYS_SEARCH_USER_PATHS = NO; 551 | CLANG_ANALYZER_NONNULL = YES; 552 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 553 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 554 | CLANG_CXX_LIBRARY = "libc++"; 555 | CLANG_ENABLE_MODULES = YES; 556 | CLANG_ENABLE_OBJC_ARC = YES; 557 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 558 | CLANG_WARN_BOOL_CONVERSION = YES; 559 | CLANG_WARN_COMMA = YES; 560 | CLANG_WARN_CONSTANT_CONVERSION = YES; 561 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 562 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 563 | CLANG_WARN_EMPTY_BODY = YES; 564 | CLANG_WARN_ENUM_CONVERSION = YES; 565 | CLANG_WARN_INFINITE_RECURSION = YES; 566 | CLANG_WARN_INT_CONVERSION = YES; 567 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 568 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 569 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 570 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 571 | CLANG_WARN_STRICT_PROTOTYPES = YES; 572 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 573 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 574 | CLANG_WARN_UNREACHABLE_CODE = YES; 575 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 576 | CODE_SIGN_IDENTITY = "iPhone Developer"; 577 | COPY_PHASE_STRIP = NO; 578 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 579 | ENABLE_NS_ASSERTIONS = NO; 580 | ENABLE_STRICT_OBJC_MSGSEND = YES; 581 | GCC_C_LANGUAGE_STANDARD = gnu11; 582 | GCC_NO_COMMON_BLOCKS = YES; 583 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 584 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 585 | GCC_WARN_UNDECLARED_SELECTOR = YES; 586 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 587 | GCC_WARN_UNUSED_FUNCTION = YES; 588 | GCC_WARN_UNUSED_VARIABLE = YES; 589 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 590 | MTL_ENABLE_DEBUG_INFO = NO; 591 | SDKROOT = iphoneos; 592 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 593 | VALIDATE_PRODUCT = YES; 594 | }; 595 | name = Release; 596 | }; 597 | 01A279F620735B6F0045DE1C /* Debug */ = { 598 | isa = XCBuildConfiguration; 599 | baseConfigurationReference = FF129F2F693F8BE2354CB5E2 /* Pods-MVVM.debug.xcconfig */; 600 | buildSettings = { 601 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 602 | CODE_SIGN_STYLE = Automatic; 603 | DEVELOPMENT_TEAM = DZBBP5D6DJ; 604 | INFOPLIST_FILE = MVVM/Info.plist; 605 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 606 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 607 | PRODUCT_BUNDLE_IDENTIFIER = kcin.nil.MVVM; 608 | PRODUCT_NAME = "$(TARGET_NAME)"; 609 | SWIFT_VERSION = 4.0; 610 | TARGETED_DEVICE_FAMILY = "1,2"; 611 | }; 612 | name = Debug; 613 | }; 614 | 01A279F720735B6F0045DE1C /* Release */ = { 615 | isa = XCBuildConfiguration; 616 | baseConfigurationReference = 41924B0A77A02B66C1CE8545 /* Pods-MVVM.release.xcconfig */; 617 | buildSettings = { 618 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 619 | CODE_SIGN_STYLE = Automatic; 620 | DEVELOPMENT_TEAM = DZBBP5D6DJ; 621 | INFOPLIST_FILE = MVVM/Info.plist; 622 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 623 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 624 | PRODUCT_BUNDLE_IDENTIFIER = kcin.nil.MVVM; 625 | PRODUCT_NAME = "$(TARGET_NAME)"; 626 | SWIFT_VERSION = 4.0; 627 | TARGETED_DEVICE_FAMILY = "1,2"; 628 | }; 629 | name = Release; 630 | }; 631 | /* End XCBuildConfiguration section */ 632 | 633 | /* Begin XCConfigurationList section */ 634 | 01A279DE20735B6F0045DE1C /* Build configuration list for PBXProject "MVVM" */ = { 635 | isa = XCConfigurationList; 636 | buildConfigurations = ( 637 | 01A279F320735B6F0045DE1C /* Debug */, 638 | 01A279F420735B6F0045DE1C /* Release */, 639 | ); 640 | defaultConfigurationIsVisible = 0; 641 | defaultConfigurationName = Release; 642 | }; 643 | 01A279F520735B6F0045DE1C /* Build configuration list for PBXNativeTarget "MVVM" */ = { 644 | isa = XCConfigurationList; 645 | buildConfigurations = ( 646 | 01A279F620735B6F0045DE1C /* Debug */, 647 | 01A279F720735B6F0045DE1C /* Release */, 648 | ); 649 | defaultConfigurationIsVisible = 0; 650 | defaultConfigurationName = Release; 651 | }; 652 | /* End XCConfigurationList section */ 653 | }; 654 | rootObject = 01A279DB20735B6F0045DE1C /* Project object */; 655 | } 656 | --------------------------------------------------------------------------------