├── .gitignore ├── IMG_3630.PNG ├── LICENSE ├── README.md ├── XLStockChart ├── XLStockChart.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── XLStockChart │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── ViewController.swift │ ├── XLCrossDetailView.swift │ ├── XLStockChart │ │ ├── DrawLayerProtocol │ │ │ └── XLDrawLayerProtocol.swift │ │ ├── Helpers │ │ │ ├── Extension.swift │ │ │ ├── XLKlineStyle.swift │ │ │ └── XLStockChartNameSpace.swift │ │ └── KLineChart │ │ │ ├── XLBottomChartTextLayer.swift │ │ │ ├── XLCrossLineLayer.swift │ │ │ ├── XLHighLowTextLayer.swift │ │ │ ├── XLKLine.swift │ │ │ ├── XLKLineCoordModel.swift │ │ │ ├── XLKLineModel.swift │ │ │ ├── XLKLineScrollView.swift │ │ │ ├── XLKLineView.swift │ │ │ ├── XLMidChartTextLayer.swift │ │ │ └── XLTopChartTextLayer.swift │ └── data │ │ ├── 15MinData.json │ │ ├── 1HourData.json │ │ ├── 1WeekData.json │ │ ├── 1dayData.json │ │ ├── 5MinData.json │ │ └── minLineData.json ├── XLStockChartTests │ ├── Info.plist │ └── XLStockChartTests.swift └── XLStockChartUITests │ ├── Info.plist │ └── XLStockChartUITests.swift └── demo.gif /.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 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /IMG_3630.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sum123/XLStockChart/fcd29980c12c2cf61f8391ebf016f8145dcaaa3c/IMG_3630.PNG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sum123 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XLStockChart 2 | K线图(烛线图,分时图,MA、VOL、MACD、KDJ、RSI) 捏合手势 加载更多等等 3 | 4 | ![iOS 9.0+](https://img.shields.io/badge/iOS-9.0%2B-blue.svg) 5 | ![Swift 4.0+](https://img.shields.io/badge/Swift-4.0%2B-orange.svg) 6 | ![MIT](https://img.shields.io/github/license/mashape/apistatus.svg) 7 | 8 | XLStockChart是一个用于区块链币价或股票行情显示的库。 9 | 10 | ![](https://github.com/Sum123/XLStockChart/blob/master/IMG_3630.PNG?raw=true) 11 | ![](https://github.com/Sum123/XLStockChart/blob/master/demo.gif?raw=true) 12 | 13 | ## Example 14 | // 日期显示类型 日K以内是MM/DD HH:mm 日K以外是YY/MM/DD 15 | dateType = .min 16 | 17 | // K线类型 烛线图/分时图 18 | lineType = .candleLineType 19 | 20 | // 主图显示 默认MA 21 | var mainString = KLINEMA 22 | 23 | // 副图显示 默认VOL 24 | var secondString = KLINEVOL 25 | 26 | var dataArray = [XLKLineModel]() 27 | 28 | switch sender.currentTitle { 29 | case KLINETIMEMinLine: 30 | // 分时 31 | dataArray = getModelArrayFromFile("minLineData") 32 | lineType = .minLineType 33 | case KLINETIME5Min: 34 | // 5分钟 35 | dataArray = getModelArrayFromFile("5MinData") 36 | case KLINETIME15Min: 37 | // 15分钟 38 | dataArray = getModelArrayFromFile("15MinData") 39 | case KLINETIME1Hour: 40 | // 1小时 41 | dataArray = getModelArrayFromFile("1HourData") 42 | case KLINETIME1Day: 43 | // 日K 44 | dataArray = getModelArrayFromFile("1dayData") 45 | dateType = .day 46 | case KLINETIME1Week: 47 | // 周K 48 | dataArray = getModelArrayFromFile("1WeekData") 49 | dateType = .day 50 | default: 51 | break 52 | } 53 | 54 | if dataArray.count > 0 { 55 | // 模拟网络请求 56 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 57 | self.kLineView.configureView(data: dataArray, isNew: true, mainDrawString: mainString, secondDrawString: secondString, dateType: dateType, lineType: lineType) 58 | } 59 | } 60 | 61 | 62 | ## Features 63 | 1. 支持`分时图`、`烛线图`、`MA`、`VOL`、`MACD`、`KDJ`、`RSI`、`最高&最低`等数据展示。 64 | 2. 支持横屏查看。 65 | 3. 手势捏合、长按展示详情、加载更多数据等。 66 | 4. K线图利用 `UIScrollView` 达到流畅的滑动查看效果, 减少重绘渲染次数。 67 | 5. 使用 `CAShapeLayer` 绘图,内存占用更小,效率更高。 68 | 69 | ## Requirements 70 | 71 | - iOS 9.0+ 72 | - Swift 4 -------------------------------------------------------------------------------- /XLStockChart/XLStockChart.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C3189BF921A4340D00959EDF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189BF821A4340D00959EDF /* AppDelegate.swift */; }; 11 | C3189BFB21A4340D00959EDF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189BFA21A4340D00959EDF /* ViewController.swift */; }; 12 | C3189BFE21A4340D00959EDF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C3189BFC21A4340D00959EDF /* Main.storyboard */; }; 13 | C3189C0021A4340E00959EDF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3189BFF21A4340E00959EDF /* Assets.xcassets */; }; 14 | C3189C0321A4340E00959EDF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C3189C0121A4340E00959EDF /* LaunchScreen.storyboard */; }; 15 | C3189C0E21A4340E00959EDF /* XLStockChartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C0D21A4340E00959EDF /* XLStockChartTests.swift */; }; 16 | C3189C1921A4340E00959EDF /* XLStockChartUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C1821A4340E00959EDF /* XLStockChartUITests.swift */; }; 17 | C3189C2721A4344000959EDF /* XLCrossDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C2621A4344000959EDF /* XLCrossDetailView.swift */; }; 18 | C3189C2F21A4344C00959EDF /* 1dayData.json in Resources */ = {isa = PBXBuildFile; fileRef = C3189C2921A4344C00959EDF /* 1dayData.json */; }; 19 | C3189C3021A4344C00959EDF /* 1WeekData.json in Resources */ = {isa = PBXBuildFile; fileRef = C3189C2A21A4344C00959EDF /* 1WeekData.json */; }; 20 | C3189C3121A4344C00959EDF /* 5MinData.json in Resources */ = {isa = PBXBuildFile; fileRef = C3189C2B21A4344C00959EDF /* 5MinData.json */; }; 21 | C3189C3221A4344C00959EDF /* 1HourData.json in Resources */ = {isa = PBXBuildFile; fileRef = C3189C2C21A4344C00959EDF /* 1HourData.json */; }; 22 | C3189C3321A4344C00959EDF /* minLineData.json in Resources */ = {isa = PBXBuildFile; fileRef = C3189C2D21A4344C00959EDF /* minLineData.json */; }; 23 | C3189C3421A4344C00959EDF /* 15MinData.json in Resources */ = {isa = PBXBuildFile; fileRef = C3189C2E21A4344C00959EDF /* 15MinData.json */; }; 24 | C3189C4721A4345200959EDF /* XLKLineScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3721A4345200959EDF /* XLKLineScrollView.swift */; }; 25 | C3189C4821A4345200959EDF /* XLTopChartTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3821A4345200959EDF /* XLTopChartTextLayer.swift */; }; 26 | C3189C4921A4345200959EDF /* XLBottomChartTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3921A4345200959EDF /* XLBottomChartTextLayer.swift */; }; 27 | C3189C4A21A4345200959EDF /* XLKLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3A21A4345200959EDF /* XLKLine.swift */; }; 28 | C3189C4B21A4345200959EDF /* XLKLineModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3B21A4345200959EDF /* XLKLineModel.swift */; }; 29 | C3189C4C21A4345200959EDF /* XLHighLowTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3C21A4345200959EDF /* XLHighLowTextLayer.swift */; }; 30 | C3189C4D21A4345200959EDF /* XLKLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3D21A4345200959EDF /* XLKLineView.swift */; }; 31 | C3189C4E21A4345200959EDF /* XLCrossLineLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3E21A4345200959EDF /* XLCrossLineLayer.swift */; }; 32 | C3189C4F21A4345200959EDF /* XLKLineCoordModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C3F21A4345200959EDF /* XLKLineCoordModel.swift */; }; 33 | C3189C5021A4345200959EDF /* XLMidChartTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C4021A4345200959EDF /* XLMidChartTextLayer.swift */; }; 34 | C3189C5121A4345200959EDF /* XLDrawLayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C4221A4345200959EDF /* XLDrawLayerProtocol.swift */; }; 35 | C3189C5221A4345200959EDF /* XLStockChartNameSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C4421A4345200959EDF /* XLStockChartNameSpace.swift */; }; 36 | C3189C5321A4345200959EDF /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C4521A4345200959EDF /* Extension.swift */; }; 37 | C3189C5421A4345200959EDF /* XLKlineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3189C4621A4345200959EDF /* XLKlineStyle.swift */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXContainerItemProxy section */ 41 | C3189C0A21A4340E00959EDF /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = C3189BED21A4340D00959EDF /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = C3189BF421A4340D00959EDF; 46 | remoteInfo = XLStockChart; 47 | }; 48 | C3189C1521A4340E00959EDF /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = C3189BED21A4340D00959EDF /* Project object */; 51 | proxyType = 1; 52 | remoteGlobalIDString = C3189BF421A4340D00959EDF; 53 | remoteInfo = XLStockChart; 54 | }; 55 | /* End PBXContainerItemProxy section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | C3189BF521A4340D00959EDF /* XLStockChart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XLStockChart.app; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | C3189BF821A4340D00959EDF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 60 | C3189BFA21A4340D00959EDF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 61 | C3189BFD21A4340D00959EDF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 62 | C3189BFF21A4340E00959EDF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 63 | C3189C0221A4340E00959EDF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 64 | C3189C0421A4340E00959EDF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 65 | C3189C0921A4340E00959EDF /* XLStockChartTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XLStockChartTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | C3189C0D21A4340E00959EDF /* XLStockChartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XLStockChartTests.swift; sourceTree = ""; }; 67 | C3189C0F21A4340E00959EDF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68 | C3189C1421A4340E00959EDF /* XLStockChartUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XLStockChartUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | C3189C1821A4340E00959EDF /* XLStockChartUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XLStockChartUITests.swift; sourceTree = ""; }; 70 | C3189C1A21A4340E00959EDF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | C3189C2621A4344000959EDF /* XLCrossDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLCrossDetailView.swift; sourceTree = ""; }; 72 | C3189C2921A4344C00959EDF /* 1dayData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 1dayData.json; sourceTree = ""; }; 73 | C3189C2A21A4344C00959EDF /* 1WeekData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 1WeekData.json; sourceTree = ""; }; 74 | C3189C2B21A4344C00959EDF /* 5MinData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 5MinData.json; sourceTree = ""; }; 75 | C3189C2C21A4344C00959EDF /* 1HourData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 1HourData.json; sourceTree = ""; }; 76 | C3189C2D21A4344C00959EDF /* minLineData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = minLineData.json; sourceTree = ""; }; 77 | C3189C2E21A4344C00959EDF /* 15MinData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 15MinData.json; sourceTree = ""; }; 78 | C3189C3721A4345200959EDF /* XLKLineScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLKLineScrollView.swift; sourceTree = ""; }; 79 | C3189C3821A4345200959EDF /* XLTopChartTextLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLTopChartTextLayer.swift; sourceTree = ""; }; 80 | C3189C3921A4345200959EDF /* XLBottomChartTextLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLBottomChartTextLayer.swift; sourceTree = ""; }; 81 | C3189C3A21A4345200959EDF /* XLKLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLKLine.swift; sourceTree = ""; }; 82 | C3189C3B21A4345200959EDF /* XLKLineModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLKLineModel.swift; sourceTree = ""; }; 83 | C3189C3C21A4345200959EDF /* XLHighLowTextLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLHighLowTextLayer.swift; sourceTree = ""; }; 84 | C3189C3D21A4345200959EDF /* XLKLineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLKLineView.swift; sourceTree = ""; }; 85 | C3189C3E21A4345200959EDF /* XLCrossLineLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLCrossLineLayer.swift; sourceTree = ""; }; 86 | C3189C3F21A4345200959EDF /* XLKLineCoordModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLKLineCoordModel.swift; sourceTree = ""; }; 87 | C3189C4021A4345200959EDF /* XLMidChartTextLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLMidChartTextLayer.swift; sourceTree = ""; }; 88 | C3189C4221A4345200959EDF /* XLDrawLayerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLDrawLayerProtocol.swift; sourceTree = ""; }; 89 | C3189C4421A4345200959EDF /* XLStockChartNameSpace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLStockChartNameSpace.swift; sourceTree = ""; }; 90 | C3189C4521A4345200959EDF /* Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; }; 91 | C3189C4621A4345200959EDF /* XLKlineStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLKlineStyle.swift; sourceTree = ""; }; 92 | /* End PBXFileReference section */ 93 | 94 | /* Begin PBXFrameworksBuildPhase section */ 95 | C3189BF221A4340D00959EDF /* Frameworks */ = { 96 | isa = PBXFrameworksBuildPhase; 97 | buildActionMask = 2147483647; 98 | files = ( 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | C3189C0621A4340E00959EDF /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | C3189C1121A4340E00959EDF /* Frameworks */ = { 110 | isa = PBXFrameworksBuildPhase; 111 | buildActionMask = 2147483647; 112 | files = ( 113 | ); 114 | runOnlyForDeploymentPostprocessing = 0; 115 | }; 116 | /* End PBXFrameworksBuildPhase section */ 117 | 118 | /* Begin PBXGroup section */ 119 | C3189BEC21A4340D00959EDF = { 120 | isa = PBXGroup; 121 | children = ( 122 | C3189BF721A4340D00959EDF /* XLStockChart */, 123 | C3189C0C21A4340E00959EDF /* XLStockChartTests */, 124 | C3189C1721A4340E00959EDF /* XLStockChartUITests */, 125 | C3189BF621A4340D00959EDF /* Products */, 126 | ); 127 | sourceTree = ""; 128 | }; 129 | C3189BF621A4340D00959EDF /* Products */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | C3189BF521A4340D00959EDF /* XLStockChart.app */, 133 | C3189C0921A4340E00959EDF /* XLStockChartTests.xctest */, 134 | C3189C1421A4340E00959EDF /* XLStockChartUITests.xctest */, 135 | ); 136 | name = Products; 137 | sourceTree = ""; 138 | }; 139 | C3189BF721A4340D00959EDF /* XLStockChart */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | C3189C3521A4345200959EDF /* XLStockChart */, 143 | C3189BF821A4340D00959EDF /* AppDelegate.swift */, 144 | C3189BFA21A4340D00959EDF /* ViewController.swift */, 145 | C3189C2621A4344000959EDF /* XLCrossDetailView.swift */, 146 | C3189BFC21A4340D00959EDF /* Main.storyboard */, 147 | C3189BFF21A4340E00959EDF /* Assets.xcassets */, 148 | C3189C2821A4344C00959EDF /* data */, 149 | C3189C0121A4340E00959EDF /* LaunchScreen.storyboard */, 150 | C3189C0421A4340E00959EDF /* Info.plist */, 151 | ); 152 | path = XLStockChart; 153 | sourceTree = ""; 154 | }; 155 | C3189C0C21A4340E00959EDF /* XLStockChartTests */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | C3189C0D21A4340E00959EDF /* XLStockChartTests.swift */, 159 | C3189C0F21A4340E00959EDF /* Info.plist */, 160 | ); 161 | path = XLStockChartTests; 162 | sourceTree = ""; 163 | }; 164 | C3189C1721A4340E00959EDF /* XLStockChartUITests */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | C3189C1821A4340E00959EDF /* XLStockChartUITests.swift */, 168 | C3189C1A21A4340E00959EDF /* Info.plist */, 169 | ); 170 | path = XLStockChartUITests; 171 | sourceTree = ""; 172 | }; 173 | C3189C2821A4344C00959EDF /* data */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | C3189C2921A4344C00959EDF /* 1dayData.json */, 177 | C3189C2A21A4344C00959EDF /* 1WeekData.json */, 178 | C3189C2B21A4344C00959EDF /* 5MinData.json */, 179 | C3189C2C21A4344C00959EDF /* 1HourData.json */, 180 | C3189C2D21A4344C00959EDF /* minLineData.json */, 181 | C3189C2E21A4344C00959EDF /* 15MinData.json */, 182 | ); 183 | path = data; 184 | sourceTree = ""; 185 | }; 186 | C3189C3521A4345200959EDF /* XLStockChart */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | C3189C3621A4345200959EDF /* KLineChart */, 190 | C3189C4121A4345200959EDF /* DrawLayerProtocol */, 191 | C3189C4321A4345200959EDF /* Helpers */, 192 | ); 193 | path = XLStockChart; 194 | sourceTree = ""; 195 | }; 196 | C3189C3621A4345200959EDF /* KLineChart */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | C3189C3721A4345200959EDF /* XLKLineScrollView.swift */, 200 | C3189C3821A4345200959EDF /* XLTopChartTextLayer.swift */, 201 | C3189C3921A4345200959EDF /* XLBottomChartTextLayer.swift */, 202 | C3189C3A21A4345200959EDF /* XLKLine.swift */, 203 | C3189C3B21A4345200959EDF /* XLKLineModel.swift */, 204 | C3189C3C21A4345200959EDF /* XLHighLowTextLayer.swift */, 205 | C3189C3D21A4345200959EDF /* XLKLineView.swift */, 206 | C3189C3E21A4345200959EDF /* XLCrossLineLayer.swift */, 207 | C3189C3F21A4345200959EDF /* XLKLineCoordModel.swift */, 208 | C3189C4021A4345200959EDF /* XLMidChartTextLayer.swift */, 209 | ); 210 | path = KLineChart; 211 | sourceTree = ""; 212 | }; 213 | C3189C4121A4345200959EDF /* DrawLayerProtocol */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | C3189C4221A4345200959EDF /* XLDrawLayerProtocol.swift */, 217 | ); 218 | path = DrawLayerProtocol; 219 | sourceTree = ""; 220 | }; 221 | C3189C4321A4345200959EDF /* Helpers */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | C3189C4421A4345200959EDF /* XLStockChartNameSpace.swift */, 225 | C3189C4521A4345200959EDF /* Extension.swift */, 226 | C3189C4621A4345200959EDF /* XLKlineStyle.swift */, 227 | ); 228 | path = Helpers; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXGroup section */ 232 | 233 | /* Begin PBXNativeTarget section */ 234 | C3189BF421A4340D00959EDF /* XLStockChart */ = { 235 | isa = PBXNativeTarget; 236 | buildConfigurationList = C3189C1D21A4340E00959EDF /* Build configuration list for PBXNativeTarget "XLStockChart" */; 237 | buildPhases = ( 238 | C3189BF121A4340D00959EDF /* Sources */, 239 | C3189BF221A4340D00959EDF /* Frameworks */, 240 | C3189BF321A4340D00959EDF /* Resources */, 241 | ); 242 | buildRules = ( 243 | ); 244 | dependencies = ( 245 | ); 246 | name = XLStockChart; 247 | productName = XLStockChart; 248 | productReference = C3189BF521A4340D00959EDF /* XLStockChart.app */; 249 | productType = "com.apple.product-type.application"; 250 | }; 251 | C3189C0821A4340E00959EDF /* XLStockChartTests */ = { 252 | isa = PBXNativeTarget; 253 | buildConfigurationList = C3189C2021A4340E00959EDF /* Build configuration list for PBXNativeTarget "XLStockChartTests" */; 254 | buildPhases = ( 255 | C3189C0521A4340E00959EDF /* Sources */, 256 | C3189C0621A4340E00959EDF /* Frameworks */, 257 | C3189C0721A4340E00959EDF /* Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | C3189C0B21A4340E00959EDF /* PBXTargetDependency */, 263 | ); 264 | name = XLStockChartTests; 265 | productName = XLStockChartTests; 266 | productReference = C3189C0921A4340E00959EDF /* XLStockChartTests.xctest */; 267 | productType = "com.apple.product-type.bundle.unit-test"; 268 | }; 269 | C3189C1321A4340E00959EDF /* XLStockChartUITests */ = { 270 | isa = PBXNativeTarget; 271 | buildConfigurationList = C3189C2321A4340E00959EDF /* Build configuration list for PBXNativeTarget "XLStockChartUITests" */; 272 | buildPhases = ( 273 | C3189C1021A4340E00959EDF /* Sources */, 274 | C3189C1121A4340E00959EDF /* Frameworks */, 275 | C3189C1221A4340E00959EDF /* Resources */, 276 | ); 277 | buildRules = ( 278 | ); 279 | dependencies = ( 280 | C3189C1621A4340E00959EDF /* PBXTargetDependency */, 281 | ); 282 | name = XLStockChartUITests; 283 | productName = XLStockChartUITests; 284 | productReference = C3189C1421A4340E00959EDF /* XLStockChartUITests.xctest */; 285 | productType = "com.apple.product-type.bundle.ui-testing"; 286 | }; 287 | /* End PBXNativeTarget section */ 288 | 289 | /* Begin PBXProject section */ 290 | C3189BED21A4340D00959EDF /* Project object */ = { 291 | isa = PBXProject; 292 | attributes = { 293 | LastSwiftUpdateCheck = 1010; 294 | LastUpgradeCheck = 1010; 295 | ORGANIZATIONNAME = "夏磊"; 296 | TargetAttributes = { 297 | C3189BF421A4340D00959EDF = { 298 | CreatedOnToolsVersion = 10.1; 299 | }; 300 | C3189C0821A4340E00959EDF = { 301 | CreatedOnToolsVersion = 10.1; 302 | TestTargetID = C3189BF421A4340D00959EDF; 303 | }; 304 | C3189C1321A4340E00959EDF = { 305 | CreatedOnToolsVersion = 10.1; 306 | TestTargetID = C3189BF421A4340D00959EDF; 307 | }; 308 | }; 309 | }; 310 | buildConfigurationList = C3189BF021A4340D00959EDF /* Build configuration list for PBXProject "XLStockChart" */; 311 | compatibilityVersion = "Xcode 9.3"; 312 | developmentRegion = en; 313 | hasScannedForEncodings = 0; 314 | knownRegions = ( 315 | en, 316 | Base, 317 | ); 318 | mainGroup = C3189BEC21A4340D00959EDF; 319 | productRefGroup = C3189BF621A4340D00959EDF /* Products */; 320 | projectDirPath = ""; 321 | projectRoot = ""; 322 | targets = ( 323 | C3189BF421A4340D00959EDF /* XLStockChart */, 324 | C3189C0821A4340E00959EDF /* XLStockChartTests */, 325 | C3189C1321A4340E00959EDF /* XLStockChartUITests */, 326 | ); 327 | }; 328 | /* End PBXProject section */ 329 | 330 | /* Begin PBXResourcesBuildPhase section */ 331 | C3189BF321A4340D00959EDF /* Resources */ = { 332 | isa = PBXResourcesBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | C3189C0321A4340E00959EDF /* LaunchScreen.storyboard in Resources */, 336 | C3189C0021A4340E00959EDF /* Assets.xcassets in Resources */, 337 | C3189C3021A4344C00959EDF /* 1WeekData.json in Resources */, 338 | C3189C2F21A4344C00959EDF /* 1dayData.json in Resources */, 339 | C3189C3121A4344C00959EDF /* 5MinData.json in Resources */, 340 | C3189BFE21A4340D00959EDF /* Main.storyboard in Resources */, 341 | C3189C3321A4344C00959EDF /* minLineData.json in Resources */, 342 | C3189C3421A4344C00959EDF /* 15MinData.json in Resources */, 343 | C3189C3221A4344C00959EDF /* 1HourData.json in Resources */, 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | }; 347 | C3189C0721A4340E00959EDF /* Resources */ = { 348 | isa = PBXResourcesBuildPhase; 349 | buildActionMask = 2147483647; 350 | files = ( 351 | ); 352 | runOnlyForDeploymentPostprocessing = 0; 353 | }; 354 | C3189C1221A4340E00959EDF /* Resources */ = { 355 | isa = PBXResourcesBuildPhase; 356 | buildActionMask = 2147483647; 357 | files = ( 358 | ); 359 | runOnlyForDeploymentPostprocessing = 0; 360 | }; 361 | /* End PBXResourcesBuildPhase section */ 362 | 363 | /* Begin PBXSourcesBuildPhase section */ 364 | C3189BF121A4340D00959EDF /* Sources */ = { 365 | isa = PBXSourcesBuildPhase; 366 | buildActionMask = 2147483647; 367 | files = ( 368 | C3189C4D21A4345200959EDF /* XLKLineView.swift in Sources */, 369 | C3189C4821A4345200959EDF /* XLTopChartTextLayer.swift in Sources */, 370 | C3189BFB21A4340D00959EDF /* ViewController.swift in Sources */, 371 | C3189C5121A4345200959EDF /* XLDrawLayerProtocol.swift in Sources */, 372 | C3189C4A21A4345200959EDF /* XLKLine.swift in Sources */, 373 | C3189BF921A4340D00959EDF /* AppDelegate.swift in Sources */, 374 | C3189C4F21A4345200959EDF /* XLKLineCoordModel.swift in Sources */, 375 | C3189C5421A4345200959EDF /* XLKlineStyle.swift in Sources */, 376 | C3189C5321A4345200959EDF /* Extension.swift in Sources */, 377 | C3189C5021A4345200959EDF /* XLMidChartTextLayer.swift in Sources */, 378 | C3189C5221A4345200959EDF /* XLStockChartNameSpace.swift in Sources */, 379 | C3189C2721A4344000959EDF /* XLCrossDetailView.swift in Sources */, 380 | C3189C4B21A4345200959EDF /* XLKLineModel.swift in Sources */, 381 | C3189C4E21A4345200959EDF /* XLCrossLineLayer.swift in Sources */, 382 | C3189C4921A4345200959EDF /* XLBottomChartTextLayer.swift in Sources */, 383 | C3189C4721A4345200959EDF /* XLKLineScrollView.swift in Sources */, 384 | C3189C4C21A4345200959EDF /* XLHighLowTextLayer.swift in Sources */, 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | }; 388 | C3189C0521A4340E00959EDF /* Sources */ = { 389 | isa = PBXSourcesBuildPhase; 390 | buildActionMask = 2147483647; 391 | files = ( 392 | C3189C0E21A4340E00959EDF /* XLStockChartTests.swift in Sources */, 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | C3189C1021A4340E00959EDF /* Sources */ = { 397 | isa = PBXSourcesBuildPhase; 398 | buildActionMask = 2147483647; 399 | files = ( 400 | C3189C1921A4340E00959EDF /* XLStockChartUITests.swift in Sources */, 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | }; 404 | /* End PBXSourcesBuildPhase section */ 405 | 406 | /* Begin PBXTargetDependency section */ 407 | C3189C0B21A4340E00959EDF /* PBXTargetDependency */ = { 408 | isa = PBXTargetDependency; 409 | target = C3189BF421A4340D00959EDF /* XLStockChart */; 410 | targetProxy = C3189C0A21A4340E00959EDF /* PBXContainerItemProxy */; 411 | }; 412 | C3189C1621A4340E00959EDF /* PBXTargetDependency */ = { 413 | isa = PBXTargetDependency; 414 | target = C3189BF421A4340D00959EDF /* XLStockChart */; 415 | targetProxy = C3189C1521A4340E00959EDF /* PBXContainerItemProxy */; 416 | }; 417 | /* End PBXTargetDependency section */ 418 | 419 | /* Begin PBXVariantGroup section */ 420 | C3189BFC21A4340D00959EDF /* Main.storyboard */ = { 421 | isa = PBXVariantGroup; 422 | children = ( 423 | C3189BFD21A4340D00959EDF /* Base */, 424 | ); 425 | name = Main.storyboard; 426 | sourceTree = ""; 427 | }; 428 | C3189C0121A4340E00959EDF /* LaunchScreen.storyboard */ = { 429 | isa = PBXVariantGroup; 430 | children = ( 431 | C3189C0221A4340E00959EDF /* Base */, 432 | ); 433 | name = LaunchScreen.storyboard; 434 | sourceTree = ""; 435 | }; 436 | /* End PBXVariantGroup section */ 437 | 438 | /* Begin XCBuildConfiguration section */ 439 | C3189C1B21A4340E00959EDF /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ALWAYS_SEARCH_USER_PATHS = NO; 443 | CLANG_ANALYZER_NONNULL = YES; 444 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 445 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 446 | CLANG_CXX_LIBRARY = "libc++"; 447 | CLANG_ENABLE_MODULES = YES; 448 | CLANG_ENABLE_OBJC_ARC = YES; 449 | CLANG_ENABLE_OBJC_WEAK = YES; 450 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 451 | CLANG_WARN_BOOL_CONVERSION = YES; 452 | CLANG_WARN_COMMA = YES; 453 | CLANG_WARN_CONSTANT_CONVERSION = YES; 454 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 455 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 456 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 457 | CLANG_WARN_EMPTY_BODY = YES; 458 | CLANG_WARN_ENUM_CONVERSION = YES; 459 | CLANG_WARN_INFINITE_RECURSION = YES; 460 | CLANG_WARN_INT_CONVERSION = YES; 461 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 462 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 463 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 464 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 465 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 466 | CLANG_WARN_STRICT_PROTOTYPES = YES; 467 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 468 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 469 | CLANG_WARN_UNREACHABLE_CODE = YES; 470 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 471 | CODE_SIGN_IDENTITY = "iPhone Developer"; 472 | COPY_PHASE_STRIP = NO; 473 | DEBUG_INFORMATION_FORMAT = dwarf; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | ENABLE_TESTABILITY = YES; 476 | GCC_C_LANGUAGE_STANDARD = gnu11; 477 | GCC_DYNAMIC_NO_PIC = NO; 478 | GCC_NO_COMMON_BLOCKS = YES; 479 | GCC_OPTIMIZATION_LEVEL = 0; 480 | GCC_PREPROCESSOR_DEFINITIONS = ( 481 | "DEBUG=1", 482 | "$(inherited)", 483 | ); 484 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 485 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 486 | GCC_WARN_UNDECLARED_SELECTOR = YES; 487 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 488 | GCC_WARN_UNUSED_FUNCTION = YES; 489 | GCC_WARN_UNUSED_VARIABLE = YES; 490 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 491 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 492 | MTL_FAST_MATH = YES; 493 | ONLY_ACTIVE_ARCH = YES; 494 | SDKROOT = iphoneos; 495 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 496 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 497 | }; 498 | name = Debug; 499 | }; 500 | C3189C1C21A4340E00959EDF /* Release */ = { 501 | isa = XCBuildConfiguration; 502 | buildSettings = { 503 | ALWAYS_SEARCH_USER_PATHS = NO; 504 | CLANG_ANALYZER_NONNULL = YES; 505 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 506 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 507 | CLANG_CXX_LIBRARY = "libc++"; 508 | CLANG_ENABLE_MODULES = YES; 509 | CLANG_ENABLE_OBJC_ARC = YES; 510 | CLANG_ENABLE_OBJC_WEAK = YES; 511 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 512 | CLANG_WARN_BOOL_CONVERSION = YES; 513 | CLANG_WARN_COMMA = YES; 514 | CLANG_WARN_CONSTANT_CONVERSION = YES; 515 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 516 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 517 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 518 | CLANG_WARN_EMPTY_BODY = YES; 519 | CLANG_WARN_ENUM_CONVERSION = YES; 520 | CLANG_WARN_INFINITE_RECURSION = YES; 521 | CLANG_WARN_INT_CONVERSION = YES; 522 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 523 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 524 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 525 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 526 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 527 | CLANG_WARN_STRICT_PROTOTYPES = YES; 528 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 529 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 530 | CLANG_WARN_UNREACHABLE_CODE = YES; 531 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 532 | CODE_SIGN_IDENTITY = "iPhone Developer"; 533 | COPY_PHASE_STRIP = NO; 534 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 535 | ENABLE_NS_ASSERTIONS = NO; 536 | ENABLE_STRICT_OBJC_MSGSEND = YES; 537 | GCC_C_LANGUAGE_STANDARD = gnu11; 538 | GCC_NO_COMMON_BLOCKS = YES; 539 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 540 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 541 | GCC_WARN_UNDECLARED_SELECTOR = YES; 542 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 543 | GCC_WARN_UNUSED_FUNCTION = YES; 544 | GCC_WARN_UNUSED_VARIABLE = YES; 545 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 546 | MTL_ENABLE_DEBUG_INFO = NO; 547 | MTL_FAST_MATH = YES; 548 | SDKROOT = iphoneos; 549 | SWIFT_COMPILATION_MODE = wholemodule; 550 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 551 | VALIDATE_PRODUCT = YES; 552 | }; 553 | name = Release; 554 | }; 555 | C3189C1E21A4340E00959EDF /* Debug */ = { 556 | isa = XCBuildConfiguration; 557 | buildSettings = { 558 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 559 | CODE_SIGN_STYLE = Automatic; 560 | DEVELOPMENT_TEAM = LZ4W49MD62; 561 | INFOPLIST_FILE = XLStockChart/Info.plist; 562 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 563 | LD_RUNPATH_SEARCH_PATHS = ( 564 | "$(inherited)", 565 | "@executable_path/Frameworks", 566 | ); 567 | PRODUCT_BUNDLE_IDENTIFIER = com.sum123.demo001.XLStockChart; 568 | PRODUCT_NAME = "$(TARGET_NAME)"; 569 | SWIFT_VERSION = 4.2; 570 | TARGETED_DEVICE_FAMILY = "1,2"; 571 | }; 572 | name = Debug; 573 | }; 574 | C3189C1F21A4340E00959EDF /* Release */ = { 575 | isa = XCBuildConfiguration; 576 | buildSettings = { 577 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 578 | CODE_SIGN_STYLE = Automatic; 579 | DEVELOPMENT_TEAM = LZ4W49MD62; 580 | INFOPLIST_FILE = XLStockChart/Info.plist; 581 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 582 | LD_RUNPATH_SEARCH_PATHS = ( 583 | "$(inherited)", 584 | "@executable_path/Frameworks", 585 | ); 586 | PRODUCT_BUNDLE_IDENTIFIER = com.sum123.demo001.XLStockChart; 587 | PRODUCT_NAME = "$(TARGET_NAME)"; 588 | SWIFT_VERSION = 4.2; 589 | TARGETED_DEVICE_FAMILY = "1,2"; 590 | }; 591 | name = Release; 592 | }; 593 | C3189C2121A4340E00959EDF /* Debug */ = { 594 | isa = XCBuildConfiguration; 595 | buildSettings = { 596 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 597 | BUNDLE_LOADER = "$(TEST_HOST)"; 598 | CODE_SIGN_STYLE = Automatic; 599 | DEVELOPMENT_TEAM = LZ4W49MD62; 600 | INFOPLIST_FILE = XLStockChartTests/Info.plist; 601 | LD_RUNPATH_SEARCH_PATHS = ( 602 | "$(inherited)", 603 | "@executable_path/Frameworks", 604 | "@loader_path/Frameworks", 605 | ); 606 | PRODUCT_BUNDLE_IDENTIFIER = com.sum123.demo001.XLStockChartTests; 607 | PRODUCT_NAME = "$(TARGET_NAME)"; 608 | SWIFT_VERSION = 4.2; 609 | TARGETED_DEVICE_FAMILY = "1,2"; 610 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/XLStockChart.app/XLStockChart"; 611 | }; 612 | name = Debug; 613 | }; 614 | C3189C2221A4340E00959EDF /* Release */ = { 615 | isa = XCBuildConfiguration; 616 | buildSettings = { 617 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 618 | BUNDLE_LOADER = "$(TEST_HOST)"; 619 | CODE_SIGN_STYLE = Automatic; 620 | DEVELOPMENT_TEAM = LZ4W49MD62; 621 | INFOPLIST_FILE = XLStockChartTests/Info.plist; 622 | LD_RUNPATH_SEARCH_PATHS = ( 623 | "$(inherited)", 624 | "@executable_path/Frameworks", 625 | "@loader_path/Frameworks", 626 | ); 627 | PRODUCT_BUNDLE_IDENTIFIER = com.sum123.demo001.XLStockChartTests; 628 | PRODUCT_NAME = "$(TARGET_NAME)"; 629 | SWIFT_VERSION = 4.2; 630 | TARGETED_DEVICE_FAMILY = "1,2"; 631 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/XLStockChart.app/XLStockChart"; 632 | }; 633 | name = Release; 634 | }; 635 | C3189C2421A4340E00959EDF /* Debug */ = { 636 | isa = XCBuildConfiguration; 637 | buildSettings = { 638 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 639 | CODE_SIGN_STYLE = Automatic; 640 | DEVELOPMENT_TEAM = LZ4W49MD62; 641 | INFOPLIST_FILE = XLStockChartUITests/Info.plist; 642 | LD_RUNPATH_SEARCH_PATHS = ( 643 | "$(inherited)", 644 | "@executable_path/Frameworks", 645 | "@loader_path/Frameworks", 646 | ); 647 | PRODUCT_BUNDLE_IDENTIFIER = com.sum123.demo001.XLStockChartUITests; 648 | PRODUCT_NAME = "$(TARGET_NAME)"; 649 | SWIFT_VERSION = 4.2; 650 | TARGETED_DEVICE_FAMILY = "1,2"; 651 | TEST_TARGET_NAME = XLStockChart; 652 | }; 653 | name = Debug; 654 | }; 655 | C3189C2521A4340E00959EDF /* Release */ = { 656 | isa = XCBuildConfiguration; 657 | buildSettings = { 658 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 659 | CODE_SIGN_STYLE = Automatic; 660 | DEVELOPMENT_TEAM = LZ4W49MD62; 661 | INFOPLIST_FILE = XLStockChartUITests/Info.plist; 662 | LD_RUNPATH_SEARCH_PATHS = ( 663 | "$(inherited)", 664 | "@executable_path/Frameworks", 665 | "@loader_path/Frameworks", 666 | ); 667 | PRODUCT_BUNDLE_IDENTIFIER = com.sum123.demo001.XLStockChartUITests; 668 | PRODUCT_NAME = "$(TARGET_NAME)"; 669 | SWIFT_VERSION = 4.2; 670 | TARGETED_DEVICE_FAMILY = "1,2"; 671 | TEST_TARGET_NAME = XLStockChart; 672 | }; 673 | name = Release; 674 | }; 675 | /* End XCBuildConfiguration section */ 676 | 677 | /* Begin XCConfigurationList section */ 678 | C3189BF021A4340D00959EDF /* Build configuration list for PBXProject "XLStockChart" */ = { 679 | isa = XCConfigurationList; 680 | buildConfigurations = ( 681 | C3189C1B21A4340E00959EDF /* Debug */, 682 | C3189C1C21A4340E00959EDF /* Release */, 683 | ); 684 | defaultConfigurationIsVisible = 0; 685 | defaultConfigurationName = Release; 686 | }; 687 | C3189C1D21A4340E00959EDF /* Build configuration list for PBXNativeTarget "XLStockChart" */ = { 688 | isa = XCConfigurationList; 689 | buildConfigurations = ( 690 | C3189C1E21A4340E00959EDF /* Debug */, 691 | C3189C1F21A4340E00959EDF /* Release */, 692 | ); 693 | defaultConfigurationIsVisible = 0; 694 | defaultConfigurationName = Release; 695 | }; 696 | C3189C2021A4340E00959EDF /* Build configuration list for PBXNativeTarget "XLStockChartTests" */ = { 697 | isa = XCConfigurationList; 698 | buildConfigurations = ( 699 | C3189C2121A4340E00959EDF /* Debug */, 700 | C3189C2221A4340E00959EDF /* Release */, 701 | ); 702 | defaultConfigurationIsVisible = 0; 703 | defaultConfigurationName = Release; 704 | }; 705 | C3189C2321A4340E00959EDF /* Build configuration list for PBXNativeTarget "XLStockChartUITests" */ = { 706 | isa = XCConfigurationList; 707 | buildConfigurations = ( 708 | C3189C2421A4340E00959EDF /* Debug */, 709 | C3189C2521A4340E00959EDF /* Release */, 710 | ); 711 | defaultConfigurationIsVisible = 0; 712 | defaultConfigurationName = Release; 713 | }; 714 | /* End XCConfigurationList section */ 715 | }; 716 | rootObject = C3189BED21A4340D00959EDF /* Project object */; 717 | } 718 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/11/20. 6 | // Copyright © 2018 夏磊. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/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 | } -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/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 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/Base.lproj/Main.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 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/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 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/11/16. 6 | // Copyright © 2018 夏磊. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let iPhoneX: Bool = UIApplication.shared.statusBarFrame.size.height > 20 12 | let XLStatusBarHeight: CGFloat = iPhoneX ? 44.0 : 20.0 13 | let XLNavBarHeight: CGFloat = 44.0 14 | let XLTopHeight: CGFloat = XLStatusBarHeight + XLNavBarHeight 15 | let XLBottomHeight: CGFloat = iPhoneX ? 34.0 : 0.0 16 | let XLScreenW: CGFloat = UIScreen.main.bounds.width 17 | let XLScreenH: CGFloat = UIScreen.main.bounds.height 18 | 19 | let KLINETIMEMinLine = "分时" 20 | let KLINETIME5Min = "5分钟" 21 | let KLINETIME15Min = "15分钟" 22 | let KLINETIME1Hour = "1小时" 23 | let KLINETIME1Day = "日K" 24 | let KLINETIME1Week = "周K" 25 | 26 | class ViewController: UIViewController { 27 | 28 | /// 日期显示类型 日K以内是MM/DD HH:mm 日K以外是YY/MM/DD 29 | var dateType: XLKLineDateType = .min 30 | 31 | /// K线类型 烛线图/分时图 32 | var lineType: XLKLineType = .candleLineType 33 | 34 | /// 主图显示 默认MA (MA、BOLL、隐藏) 35 | var mainString = KLINEMA 36 | 37 | /// 副图显示 默认VOL (VOL、MACD、KDJ、RSI) 38 | var secondString = KLINEVOL 39 | 40 | /// 十字线是否在动画 41 | var isCrossAnimation = false 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | view.backgroundColor = UIColor.white 47 | 48 | view.addSubview(timeView) 49 | view.addSubview(kLineView) 50 | view.addSubview(indicatorView) 51 | view.addSubview(detailView) 52 | 53 | clickTimeBtn(timeBtnArray[2]) 54 | } 55 | 56 | // MARK: - Action 57 | @objc func clickDetail() { 58 | self.kLineView.hideCross() 59 | } 60 | 61 | @objc func clickTimeBtn(_ sender: UIButton) { 62 | 63 | indicatorView.startAnimating() 64 | view.isUserInteractionEnabled = false 65 | 66 | for btn in timeBtnArray { 67 | btn.setTitleColor(UIColor.init(white: 1, alpha: 0.6), for: .normal) 68 | } 69 | sender.setTitleColor(UIColor.white, for: .normal) 70 | 71 | dateType = .min 72 | lineType = .candleLineType 73 | 74 | var dataArray = [XLKLineModel]() 75 | 76 | switch sender.currentTitle { 77 | case KLINETIMEMinLine: 78 | // 分时 79 | dataArray = getModelArrayFromFile("minLineData") 80 | lineType = .minLineType 81 | case KLINETIME5Min: 82 | // 5分钟 83 | dataArray = getModelArrayFromFile("5MinData") 84 | case KLINETIME15Min: 85 | // 15分钟 86 | dataArray = getModelArrayFromFile("15MinData") 87 | case KLINETIME1Hour: 88 | // 1小时 89 | dataArray = getModelArrayFromFile("1HourData") 90 | case KLINETIME1Day: 91 | // 日K 92 | dataArray = getModelArrayFromFile("1dayData") 93 | dateType = .day 94 | case KLINETIME1Week: 95 | // 周K 96 | dataArray = getModelArrayFromFile("1WeekData") 97 | dateType = .day 98 | default: 99 | break 100 | } 101 | 102 | if dataArray.count > 0 { 103 | // 模拟网络请求 104 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 105 | self.indicatorView.stopAnimating() 106 | self.view.isUserInteractionEnabled = true 107 | self.kLineView.configureView(data: dataArray, isNew: true, mainDrawString: self.mainString, secondDrawString: self.secondString, dateType: self.dateType, lineType: self.lineType) 108 | } 109 | } 110 | } 111 | 112 | // MARK: - Method 113 | func getModelArrayFromFile(_ fileName: String) -> [XLKLineModel] { 114 | let pathForResource = Bundle.main.path(forResource: fileName, ofType: "json") 115 | let json = try! String(contentsOfFile: pathForResource!, encoding: String.Encoding.utf8) 116 | let jsonData = json.data(using: String.Encoding.utf8)! 117 | 118 | let dict = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String : Any] 119 | 120 | let klines = (dict["data"] as! [String : Any])["klines"] as! [[String : Any]] 121 | 122 | var tempArray = [XLKLineModel]() 123 | 124 | for klineDict in klines { 125 | let model = XLKLineModel() 126 | if let open = klineDict["open"] as? CGFloat { 127 | model.open = open 128 | } 129 | if let close = klineDict["close"] as? CGFloat { 130 | model.close = close 131 | } 132 | if let high = klineDict["high"] as? CGFloat { 133 | model.high = high 134 | } 135 | if let low = klineDict["low"] as? CGFloat { 136 | model.low = low 137 | } 138 | if let volumefrom = klineDict["volumefrom"] as? CGFloat { 139 | model.volumefrom = volumefrom 140 | } 141 | if let time = klineDict["time"] as? TimeInterval { 142 | model.time = time 143 | } 144 | if let inflow = klineDict["inflow"] as? CGFloat { 145 | model.inflow = inflow 146 | } 147 | if let outflow = klineDict["outflow"] as? CGFloat { 148 | model.outflow = outflow 149 | } 150 | if let boll_mb = klineDict["boll_mb"] as? CGFloat { 151 | model.boll_mb = boll_mb 152 | } 153 | if let boll_up = klineDict["boll_up"] as? CGFloat { 154 | model.boll_up = boll_up 155 | } 156 | if let boll_dn = klineDict["boll_dn"] as? CGFloat { 157 | model.boll_dn = boll_dn 158 | } 159 | if let ma5 = klineDict["ma5"] as? CGFloat { 160 | model.ma5 = ma5 161 | } 162 | if let ma10 = klineDict["ma10"] as? CGFloat { 163 | model.ma10 = ma10 164 | } 165 | if let ma30 = klineDict["ma30"] as? CGFloat { 166 | model.ma30 = ma30 167 | } 168 | if let ma60 = klineDict["ma30"] as? CGFloat { 169 | model.ma60 = ma60 170 | } 171 | if let macd_diff = klineDict["macd_diff"] as? CGFloat { 172 | model.macd_diff = macd_diff 173 | } 174 | if let macd_dea = klineDict["macd_dea"] as? CGFloat { 175 | model.macd_dea = macd_dea 176 | } 177 | if let macd_bar = klineDict["macd_bar"] as? CGFloat { 178 | model.macd_bar = macd_bar 179 | } 180 | if let boll_dn = klineDict["boll_dn"] as? CGFloat { 181 | model.boll_dn = boll_dn 182 | } 183 | if let kdj_k = klineDict["kdj_k"] as? CGFloat { 184 | model.kdj_k = kdj_k 185 | } 186 | if let kdj_d = klineDict["kdj_d"] as? CGFloat { 187 | model.kdj_d = kdj_d 188 | } 189 | if let kdj_j = klineDict["kdj_j"] as? CGFloat { 190 | model.kdj_j = kdj_j 191 | } 192 | if let rsi = klineDict["rsi"] as? CGFloat { 193 | model.rsi = rsi 194 | } 195 | tempArray.append(model) 196 | } 197 | return tempArray 198 | } 199 | 200 | func setupCrossDetailHide(hide: Bool) { 201 | if isCrossAnimation { 202 | return 203 | } 204 | 205 | isCrossAnimation = true 206 | if hide { 207 | UIView.animate(withDuration: 0.25, animations: { 208 | self.detailView.alpha = 0 209 | }) { (_) in 210 | self.isCrossAnimation = false 211 | } 212 | }else { 213 | UIView.animate(withDuration: 0.25, animations: { 214 | self.detailView.alpha = 1 215 | }) { (_) in 216 | self.isCrossAnimation = false 217 | } 218 | } 219 | } 220 | 221 | // MARK: - Lazy 222 | lazy var kLineView: XLKLineView = { 223 | let kLineView = XLKLineView(frame: CGRect(x: 0, y: self.timeView.frame.maxY, width: XLScreenW, height: 500)) 224 | kLineView.backgroundColor = UIColor.xlChart.color(rgba: "#243245") 225 | kLineView.kLineViewDelegate = self 226 | return kLineView 227 | }() 228 | 229 | lazy var timeView: UIView = { 230 | let timeView = UIView.init(frame: CGRect(x: 0, y: XLStatusBarHeight + 70 - XLNavBarHeight, width: XLScreenW, height: XLNavBarHeight)) 231 | timeView.backgroundColor = UIColor.xlChart.color(rgba: "#243245") 232 | 233 | let times = [KLINETIMEMinLine, KLINETIME5Min, KLINETIME15Min, KLINETIME1Hour, KLINETIME1Day, KLINETIME1Week] 234 | let btnW: CGFloat = XLScreenW / CGFloat(times.count) 235 | var idx: Int = 0 236 | 237 | for str in times { 238 | let btn = UIButton() 239 | btn.setTitle(str, for: .normal) 240 | btn.titleLabel?.font = UIFont.systemFont(ofSize: 13) 241 | btn.setTitleColor(UIColor.init(white: 1, alpha: 0.6), for: .normal) 242 | btn.frame = CGRect(x: CGFloat(idx) * btnW, y: 0, width: btnW, height: timeView.bounds.size.height) 243 | btn.addTarget(self, action: #selector(clickTimeBtn(_:)), for: .touchUpInside) 244 | btn.tag = idx 245 | timeView.addSubview(btn) 246 | self.timeBtnArray.append(btn) 247 | idx += 1 248 | } 249 | return timeView 250 | }() 251 | 252 | lazy var indicatorView: UIActivityIndicatorView = { 253 | let indicatorView = UIActivityIndicatorView(style: .white) 254 | indicatorView.center = CGPoint(x: self.kLineView.bounds.width*0.5, y: self.timeView.frame.maxY + (self.kLineView.bounds.height - 54 - 24)*0.5) 255 | return indicatorView 256 | }() 257 | 258 | lazy var detailView: XLCrossDetailView = { 259 | let detailView = XLCrossDetailView(frame: CGRect(x: 0, y: XLStatusBarHeight, width: XLScreenW, height: 70)) 260 | let tap = UITapGestureRecognizer(target: self, action: #selector(clickDetail)) 261 | detailView.addGestureRecognizer(tap) 262 | detailView.alpha = 0 263 | return detailView 264 | }() 265 | 266 | lazy var timeBtnArray = [UIButton]() 267 | } 268 | 269 | extension ViewController: XLKLineViewProtocol { 270 | func XLKLineViewLoadMore() { 271 | print("加载更多....") 272 | } 273 | 274 | func XLKLineViewHideCrossDetail() { 275 | print("隐藏十字线") 276 | self.setupCrossDetailHide(hide: true) 277 | } 278 | 279 | func XLKLineViewLongPress(model: XLKLineModel, preClose: CGFloat) { 280 | print("长按显示") 281 | self.detailView.bind(model: model, preClose: preClose) 282 | self.setupCrossDetailHide(hide: false) 283 | } 284 | } 285 | 286 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLCrossDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLCrossDetailView.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/11/20. 6 | // Copyright © 2018 夏磊. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 长按详情视图 可自定义 12 | class XLCrossDetailView: UIView { 13 | 14 | // MARK: - Life Cycle 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | self.frame.size = CGSize(width: XLScreenW, height: 70) 19 | self.backgroundColor = UIColor.xlChart.color(rgba: "#1F2D3F") 20 | self.setupViews() 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | // MARK: - Method 28 | func bind(model: XLKLineModel, preClose: CGFloat) { 29 | // 时间 30 | oneLabArray[0].text = String.init(format: "%@: %@", KLINETIME, model.time.xlChart.toTimeString("MM/dd HH:mm")) 31 | 32 | // 成交量 33 | oneLabArray[1].text = String.init(format: "%@: %@", KLINEVOL, model.volumefrom.xlChart.kLineVolNumber()) 34 | 35 | // 涨跌额 36 | let changePrice = calaRiseAndFallPrice(model: model, preClose: preClose) 37 | twoLabArray[0].text = String.init(format: "%@: %@", KLINERISEANDFALLVOL, changePrice == 0 ? "--" : changePrice.xlChart.percent()) 38 | 39 | // 开盘 40 | twoLabArray[1].text = String.init(format: "%@: %@", KLINEOPEN, model.open.xlChart.kLinePriceNumber()) 41 | 42 | // 最高 43 | twoLabArray[2].text = String.init(format: "%@: %@", KLINEHIGH, model.high.xlChart.kLinePriceNumber()) 44 | 45 | // 涨跌幅 46 | let changeVol = calaRiseAndFallVol(model: model, preClose: preClose) 47 | threeLabArray[0].text = String.init(format: "%@: %@", KLINERISEANDFALLPERCENT, changeVol == 0 ? "--" : changeVol.xlChart.kLineVolNumber()) 48 | 49 | /// 收盘 50 | threeLabArray[1].text = String.init(format: "%@: %@", KLINECLOSE, model.close.xlChart.kLinePriceNumber()) 51 | 52 | // 最低 53 | threeLabArray[2].text = String.init(format: "%@: %@", KLINELOW, model.low.xlChart.kLinePriceNumber()) 54 | } 55 | 56 | 57 | func calaRiseAndFallVol(model: XLKLineModel, preClose: CGFloat) -> CGFloat { 58 | return model.close - preClose 59 | } 60 | 61 | func calaRiseAndFallPrice(model: XLKLineModel, preClose: CGFloat) -> CGFloat { 62 | if preClose == 0 { 63 | return 0 64 | } 65 | 66 | return (model.close - preClose) / preClose * 100 67 | } 68 | 69 | func setupViews() { 70 | let offset: CGFloat = 15 71 | var maxX: CGFloat = offset 72 | let topMargin: CGFloat = 10 73 | let space: CGFloat = 6 74 | let btnH: CGFloat = (self.bounds.size.height - 2*topMargin - space*2) / 3 75 | let margin: CGFloat = 0 76 | let btnW: CGFloat = (XLScreenW - offset*2 - margin*2) / 3 77 | 78 | for i in 0..<3 { 79 | maxX = offset 80 | 81 | let oneLab = UILabel() 82 | oneLab.adjustsFontSizeToFitWidth = true 83 | oneLab.frame = CGRect(x: maxX, y: topMargin + CGFloat(i) * (btnH+space), width: btnW, height: btnH) 84 | oneLab.textColor = UIColor.white 85 | oneLab.font = UIFont.systemFont(ofSize: 12) 86 | self.addSubview(oneLab) 87 | if i == 0 { 88 | oneLabArray.append(oneLab) 89 | }else if i == 1 { 90 | twoLabArray.append(oneLab) 91 | }else if i == 2 { 92 | threeLabArray.append(oneLab) 93 | } 94 | maxX = oneLab.frame.maxX 95 | 96 | 97 | let twoLab = UILabel() 98 | twoLab.adjustsFontSizeToFitWidth = true 99 | twoLab.frame = CGRect(x: maxX + margin + 10, y: topMargin + CGFloat(i) * (btnH+space), width: btnW, height: btnH) 100 | twoLab.textColor = UIColor.white 101 | twoLab.font = UIFont.systemFont(ofSize: 12) 102 | self.addSubview(twoLab) 103 | if i == 0 { 104 | oneLabArray.append(twoLab) 105 | }else if i == 1 { 106 | twoLabArray.append(twoLab) 107 | }else if i == 2 { 108 | threeLabArray.append(twoLab) 109 | } 110 | maxX = twoLab.frame.maxX 111 | 112 | 113 | if i == 1 || i == 2 { 114 | let threeLab = UILabel() 115 | threeLab.adjustsFontSizeToFitWidth = true 116 | threeLab.frame = CGRect(x: maxX + margin, y: topMargin + CGFloat(i) * (btnH+space), width: btnW, height: btnH) 117 | threeLab.textColor = UIColor.white 118 | threeLab.font = UIFont.systemFont(ofSize: 12) 119 | self.addSubview(threeLab) 120 | if i == 1 { 121 | twoLabArray.append(threeLab) 122 | }else if i == 2 { 123 | threeLabArray.append(threeLab) 124 | } 125 | } 126 | } 127 | } 128 | 129 | 130 | // MARK: - Lazy 131 | lazy var oneLineArray = [KLINETIME, KLINEVOL] 132 | lazy var twoLineArray = [KLINERISEANDFALLVOL, KLINEOPEN, KLINEHIGH] 133 | lazy var threeLineArray = [KLINERISEANDFALLPERCENT, KLINECLOSE, KLINELOW] 134 | lazy var oneLabArray = [UILabel]() 135 | lazy var twoLabArray = [UILabel]() 136 | lazy var threeLabArray = [UILabel]() 137 | } 138 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/DrawLayerProtocol/XLDrawLayerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLDrawLayerProtocol.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | public protocol XLDrawLayerProtocol { 13 | 14 | var theme: XLKLineStyle { get } 15 | 16 | func drawLine(lineWidth: CGFloat, startPoint: CGPoint, endPoint: CGPoint, strokeColor: UIColor, fillColor: UIColor, isDash: Bool, isAnimate: Bool) -> XLCAShapeLayer 17 | 18 | func drawTextLayer(frame: CGRect, text: String, foregroundColor: UIColor, backgroundColor: UIColor, fontSize: CGFloat) -> CATextLayer 19 | 20 | func getCrossLineLayer(frame: CGRect, pricePoint: CGPoint, volumePoint: CGPoint, model: AnyObject?) -> XLCAShapeLayer 21 | } 22 | 23 | extension XLDrawLayerProtocol { 24 | 25 | public var theme: XLKLineStyle { 26 | return XLKLineStyle() 27 | } 28 | 29 | public func drawLine(lineWidth: CGFloat, 30 | startPoint: CGPoint, 31 | endPoint: CGPoint, 32 | strokeColor: UIColor, 33 | fillColor: UIColor, 34 | isDash: Bool = false, 35 | isAnimate: Bool = false) -> XLCAShapeLayer { 36 | 37 | let linePath = UIBezierPath() 38 | linePath.move(to: startPoint) 39 | linePath.addLine(to: endPoint) 40 | 41 | let lineLayer = XLCAShapeLayer() 42 | lineLayer.path = linePath.cgPath 43 | lineLayer.lineWidth = lineWidth 44 | lineLayer.strokeColor = strokeColor.cgColor 45 | lineLayer.fillColor = fillColor.cgColor 46 | 47 | if isDash { 48 | lineLayer.lineDashPattern = [3, 3] 49 | } 50 | 51 | if isAnimate { 52 | let path = CABasicAnimation(keyPath: "strokeEnd") 53 | path.duration = 1.0 54 | path.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 55 | path.fromValue = 0.0 56 | path.toValue = 1.0 57 | lineLayer.add(path, forKey: "strokeEndAnimation") 58 | lineLayer.strokeEnd = 1.0 59 | } 60 | return lineLayer 61 | } 62 | 63 | public func drawTextLayer(frame: CGRect, 64 | text: String, 65 | foregroundColor: UIColor, 66 | backgroundColor: UIColor = UIColor.clear, 67 | fontSize: CGFloat = 10) -> CATextLayer { 68 | 69 | let textLayer = CATextLayer() 70 | textLayer.frame = frame 71 | textLayer.string = text 72 | textLayer.fontSize = fontSize 73 | textLayer.foregroundColor = foregroundColor.cgColor 74 | textLayer.backgroundColor = backgroundColor.cgColor 75 | textLayer.alignmentMode = .center 76 | textLayer.contentsScale = UIScreen.main.scale 77 | return textLayer 78 | } 79 | 80 | /// 十字线的文字layer offsetY是绘制text间距 81 | public func drawCrossTextLayer(frame: CGRect, text: String, foregroundColor: UIColor, offsetY: CGFloat = 0, backgroundColor: UIColor = UIColor.clear, fontSize: CGFloat = 10) -> XLCAShapeLayer { 82 | 83 | let bgLayer = XLCAShapeLayer() 84 | bgLayer.frame = frame 85 | bgLayer.backgroundColor = backgroundColor.cgColor 86 | bgLayer.borderColor = theme.crossBorderColor 87 | bgLayer.borderWidth = 1 88 | bgLayer.cornerRadius = 2 89 | bgLayer.contentsScale = UIScreen.main.scale 90 | 91 | let textLayer = drawTextLayer(frame: CGRect(x: 6, y: 6 - offsetY, width: frame.width - 12, height: frame.height - 12 + offsetY), text: text, foregroundColor: foregroundColor) 92 | bgLayer.addSublayer(textLayer) 93 | 94 | return bgLayer 95 | } 96 | 97 | 98 | /// 获取纵轴的标签图层 99 | func getYAxisMarkLayer(frame: CGRect, text: String, y: CGFloat, isLeft: Bool) -> CATextLayer { 100 | let textSize = theme.getTextSize(text: text) 101 | let yAxisLabelEdgeInset: CGFloat = 5 102 | var labelX: CGFloat = 0 103 | 104 | if isLeft { 105 | labelX = yAxisLabelEdgeInset 106 | } else { 107 | labelX = frame.width - textSize.width - yAxisLabelEdgeInset 108 | } 109 | 110 | let labelY: CGFloat = y - textSize.height / 2.0 111 | 112 | let yMarkLayer = drawTextLayer(frame: CGRect(x: labelX, y: labelY, width: textSize.width, height: textSize.height), text: text, foregroundColor: theme.textColor) 113 | 114 | return yMarkLayer 115 | } 116 | 117 | /// 获取长按显示的十字线及其标签图层 118 | public func getCrossLineLayer(frame: CGRect, pricePoint: CGPoint, volumePoint: CGPoint, model: AnyObject?) -> XLCAShapeLayer { 119 | let highlightLayer = XLCAShapeLayer() 120 | 121 | let corssLineLayer = XLCAShapeLayer() 122 | var yAxisMarkLayer = XLCAShapeLayer() 123 | var bottomMarkLayer = XLCAShapeLayer() 124 | var bottomMarkerString = "" 125 | var yAxisMarkString = "" 126 | 127 | guard let model = model else { return highlightLayer } 128 | 129 | if model.isKind(of: XLKLineModel.self) { 130 | let entity = model as! XLKLineModel 131 | yAxisMarkString = entity.close.xlChart.kLinePriceNumber() 132 | bottomMarkerString = "\(entity.time)" 133 | } else{ 134 | return highlightLayer 135 | } 136 | 137 | let linePath = UIBezierPath() 138 | 139 | // 竖线 140 | linePath.move(to: CGPoint(x: pricePoint.x, y: theme.topTextHeight)) 141 | linePath.addLine(to: CGPoint(x: pricePoint.x, y: frame.height)) 142 | 143 | // 横线 144 | linePath.move(to: CGPoint(x: frame.minX, y: pricePoint.y)) 145 | linePath.addLine(to: CGPoint(x: frame.maxX, y: pricePoint.y)) 146 | 147 | corssLineLayer.lineWidth = theme.corssLineWidth 148 | corssLineLayer.strokeColor = theme.crossLineColor.cgColor 149 | corssLineLayer.fillColor = theme.crossLineColor.cgColor 150 | corssLineLayer.path = linePath.cgPath 151 | 152 | // 标记标签大小 153 | let yAxisMarkSize = theme.getCrossTextSize(text: yAxisMarkString) 154 | // let volMarkSize = theme.getTextSize(text: volumeMarkerString) 155 | let bottomMarkSize = theme.getCrossTextSize(text: bottomMarkerString) 156 | 157 | var labelX: CGFloat = 0 158 | var labelY: CGFloat = 0 159 | 160 | // 纵坐标标签 161 | if pricePoint.x > frame.width / 2 { 162 | labelX = frame.maxX - yAxisMarkSize.width - 2 163 | } else { 164 | labelX = frame.minX + 2 165 | } 166 | labelY = pricePoint.y - yAxisMarkSize.height / 2.0 167 | 168 | yAxisMarkLayer = drawCrossTextLayer(frame: CGRect(x: labelX, y: labelY, width: yAxisMarkSize.width, height: theme.crossTextLayerHeight), text: yAxisMarkString, foregroundColor: theme.textColor, backgroundColor: theme.crossBgColor) 169 | 170 | // 底部时间标签 171 | let maxX = frame.maxX - bottomMarkSize.width 172 | labelX = pricePoint.x - bottomMarkSize.width / 2.0 173 | labelY = frame.height - theme.timeTextHeight + 1 174 | if labelX > maxX { 175 | labelX = frame.maxX - bottomMarkSize.width 176 | } else if labelX < frame.minX { 177 | labelX = frame.minX 178 | } 179 | 180 | bottomMarkLayer = drawCrossTextLayer(frame: CGRect(x: labelX, y: labelY, width: bottomMarkSize.width, height: theme.crossTextLayerHeight - 1), text: bottomMarkerString, foregroundColor: theme.textColor, offsetY: 1, backgroundColor: theme.crossBgColor) 181 | 182 | highlightLayer.addSublayer(corssLineLayer) 183 | highlightLayer.addSublayer(yAxisMarkLayer) 184 | highlightLayer.addSublayer(bottomMarkLayer) 185 | 186 | return highlightLayer 187 | } 188 | 189 | func getTextSize(text: String?, fontSize: CGFloat = 10, addOnWith: CGFloat = 5, addOnHeight: CGFloat = 0) -> CGSize { 190 | if let text = text { 191 | let size = text.size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)]) 192 | let width = ceil(size.width) + addOnWith 193 | let height = ceil(size.height) + addOnHeight 194 | 195 | return CGSize(width: width, height: height) 196 | }else { 197 | return CGSize.zero 198 | } 199 | } 200 | 201 | /// 成交量 MA计算 从当前屏幕最后一个数据往前num个数据的vol平均值 isMustHasNum:是否必须够num的个数 202 | public func calcVolMa(num: Int, targetIndex: Int, isMustHasNum: Bool = false, dataK: [XLKLineModel]) -> CGFloat? { 203 | if targetIndex <= 0 { 204 | return nil 205 | } 206 | 207 | if isMustHasNum, targetIndex - (num - 1) < 0 { 208 | return nil 209 | } 210 | 211 | var tempIndex: Int = targetIndex 212 | var totailvalue:CGFloat = 0 213 | 214 | // 起始位置 215 | let startIdx = max(targetIndex - (num - 1), 0) 216 | // 参与计算的值的个数 217 | var valueNum = 0 218 | 219 | while tempIndex >= startIdx { 220 | if tempIndex < dataK.count { 221 | let data = dataK[tempIndex] 222 | totailvalue += data.volumefrom 223 | tempIndex -= 1 224 | valueNum += 1 225 | } 226 | } 227 | 228 | if valueNum > 0 { 229 | return totailvalue / CGFloat(valueNum) 230 | }else { 231 | return nil 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/Helpers/Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extension.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | // MARK: - UIColor Extension 13 | extension UIColor: NameSpaceProtocol {} 14 | extension NameSpaceWrapper where T: UIColor { 15 | 16 | public static func color(rgba: String) -> UIColor { 17 | var red: CGFloat = 0.0 18 | var green: CGFloat = 0.0 19 | var blue: CGFloat = 0.0 20 | var alpha: CGFloat = 1.0 21 | 22 | if rgba.hasPrefix("#") { 23 | var hexStr = (rgba as NSString).substring(from: 1) as NSString 24 | if hexStr.length == 8 { 25 | let alphaHexStr = hexStr.substring(from: 6) 26 | hexStr = hexStr.substring(to: 6) as NSString 27 | var alphaHexValue: UInt32 = 0 28 | let alphaScanner = Scanner(string: alphaHexStr) 29 | if alphaScanner.scanHexInt32(&alphaHexValue) { 30 | let alphaHex = Int(alphaHexValue) 31 | alpha = CGFloat(alphaHex & 0x000000FF) / 255.0 32 | } else { 33 | print("scan alphaHex error") 34 | } 35 | } 36 | 37 | let rgbScanner = Scanner(string: hexStr as String) 38 | var hexValue: UInt32 = 0 39 | if rgbScanner.scanHexInt32(&hexValue) { 40 | if hexStr.length == 6 { 41 | let hex = Int(hexValue) 42 | red = CGFloat((hex & 0xFF0000) >> 16) / 255.0 43 | green = CGFloat((hex & 0x00FF00) >> 8) / 255.0 44 | blue = CGFloat(hex & 0x0000FF) / 255.0 45 | } else { 46 | print("invalid rgb string, length should be 6") 47 | } 48 | } else { 49 | print("scan hex error") 50 | } 51 | 52 | } else { 53 | print("invalid rgb string, missing '#' as prefix") 54 | } 55 | 56 | return UIColor(red:red, green:green, blue:blue, alpha:alpha) 57 | } 58 | } 59 | 60 | 61 | // MARK: - CGFloat Extension 62 | extension CGFloat: NameSpaceProtocol {} 63 | extension NameSpaceWrapper where T == CGFloat { 64 | 65 | /// 价格显示策略 >1 余2位,>0.01 余4位,>0.001 余6位,其余 余8位 66 | public func kLinePriceNumber() -> String { 67 | if wrappedValue == 0 { 68 | return "0" 69 | }else { 70 | let tempValue = abs(wrappedValue) 71 | if tempValue > 1 { 72 | return String.init(format: "%.2f", wrappedValue) 73 | }else if tempValue > 0.01 { 74 | return String.init(format: "%.4f", wrappedValue) 75 | }else if tempValue > 0.0001 { 76 | return String.init(format: "%.6f", wrappedValue) 77 | }else { 78 | return String.init(format: "%.8f", wrappedValue) 79 | } 80 | } 81 | } 82 | 83 | public func kLineVolNumber() -> String { 84 | if wrappedValue == 0 { 85 | return "未知" 86 | }else { 87 | let tempValue = abs(wrappedValue) 88 | if tempValue < 10000 { 89 | return String.init(format: "%.2f", wrappedValue) 90 | }else if tempValue < 1000000 { 91 | return String.init(format: "%.2f万", wrappedValue / 10000.0) 92 | }else if tempValue < 100000000 { 93 | return String.init(format: "%zd万", (Int)(wrappedValue / 10000.0)) 94 | }else { 95 | return String.init(format: "%.2f亿", wrappedValue / 100000000.0) 96 | } 97 | } 98 | } 99 | 100 | public func percent() -> String { 101 | if wrappedValue > 0 { 102 | return String.init(format: "+%.2f%%", wrappedValue) 103 | } else { 104 | return String.init(format: "%.2f%%", wrappedValue) 105 | } 106 | } 107 | } 108 | 109 | // MARK: - DateFormatter Extension 110 | private var cachedFormatters = [String: DateFormatter]() 111 | extension DateFormatter: NameSpaceProtocol {} 112 | extension NameSpaceWrapper where T: DateFormatter { 113 | 114 | public static func cached(withFormat format: String) -> DateFormatter { 115 | if let cachedFormatter = cachedFormatters[format] { return cachedFormatter } 116 | let formatter = DateFormatter() 117 | formatter.dateFormat = format 118 | cachedFormatters[format] = formatter 119 | return formatter 120 | } 121 | } 122 | 123 | 124 | // MARK: - Date Extension 125 | extension Date: NameSpaceProtocol {} 126 | extension NameSpaceWrapper where T == Date { 127 | 128 | public func toString(_ format: String) -> String { 129 | let dateformatter = DateFormatter.xlChart.cached(withFormat: format) 130 | dateformatter.timeZone = TimeZone.autoupdatingCurrent 131 | 132 | return dateformatter.string(from: wrappedValue) 133 | } 134 | 135 | public static func toDate(_ dateString: String, format: String) -> Date { 136 | let dateformatter = DateFormatter.xlChart.cached(withFormat: format) 137 | dateformatter.locale = Locale(identifier: "en_US") 138 | let date = dateformatter.date(from: dateString) ?? Date() 139 | 140 | return date 141 | } 142 | } 143 | 144 | 145 | // MARK: - Double Extension 146 | extension Double: NameSpaceProtocol {} 147 | extension NameSpaceWrapper where T == Double { 148 | 149 | /// 时间戳转string 150 | public func toTimeString(_ format: String) -> String { 151 | let myDate = Date.init(timeIntervalSince1970: wrappedValue) 152 | let dateformatter = DateFormatter.xlChart.cached(withFormat: format) 153 | dateformatter.timeZone = TimeZone.autoupdatingCurrent 154 | return dateformatter.string(from: myDate) 155 | } 156 | } 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/Helpers/XLKlineStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLKlineStyle.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// K线/分时类型 12 | public enum XLKLineType: Int { 13 | case minLineType = 0 // 分时类型 14 | case candleLineType = 1 // 烛线类型 15 | } 16 | 17 | /// K线时间显示格式 18 | public enum XLKLineDateType: Int { 19 | case min = 0 // 分钟显示 20 | case day = 1 // 日期显示 21 | } 22 | 23 | public let KLINEMA = "均线" 24 | public let KLINEBOLL = "BOLL" 25 | public let KLINEHIDE = "隐藏" 26 | public let KLINEVOL = "成交量" 27 | public let KLINEMACD = "MACD" 28 | public let KLINEKDJ = "KDJ" 29 | public let KLINERSI = "RSI" 30 | public let KLINETIME = "时 间" 31 | public let KLINEOPEN = "开 盘" 32 | public let KLINEHIGH = "最 高" 33 | public let KLINELOW = "最 低" 34 | public let KLINECLOSE = "收 盘" 35 | public let KLINERISEANDFALLVOL = "涨跌额" 36 | public let KLINERISEANDFALLPERCENT = "涨跌幅" 37 | 38 | public class XLKLineStyle: NSObject { 39 | 40 | let topTextHeight: CGFloat = 14 41 | let midTextHeight: CGFloat = 14 42 | let bottomChartHeight: CGFloat = 54 43 | let timeTextHeight: CGFloat = 24 44 | 45 | let corssLineWidth: CGFloat = 0.47 46 | let highLowLineWidth: CGFloat = 0.47 47 | let frameWidth: CGFloat = 0.47 48 | var crossCircleWidth: CGFloat = 8 49 | 50 | var candleWidth: CGFloat = 5 51 | var candleGap: CGFloat = 2 52 | var candleMinHeight: CGFloat = 0.5 53 | var candleMaxWidth: CGFloat = 15 54 | var candleMinWidth: CGFloat = 2 55 | 56 | // 成交量最小高度 57 | var minVolHeight: CGFloat = 0.5 58 | 59 | // topChart 上下间距 60 | let topChartMinYGap: CGFloat = 8 61 | 62 | // bottomChart 间距 63 | let volumeGap: CGFloat = 3 64 | 65 | // timeLine 上下间距 66 | let timeLineMinGap: CGFloat = 10 67 | 68 | 69 | // macdBar的宽度 70 | let macdBarWidth: CGFloat = 1 71 | 72 | let topTextOneColor = UIColor.xlChart.color(rgba: "#EDBB3C") 73 | let topTextTwoColor = UIColor.xlChart.color(rgba: "#49ADFF") 74 | let topTextThreeColor = UIColor.xlChart.color(rgba: "#DC66ED") 75 | 76 | let bottomTextOneColor = UIColor.xlChart.color(rgba: "#FFFFFF") 77 | let bottomTextTwoColor = UIColor.xlChart.color(rgba: "#EDBB3C") 78 | let bottomTextThreeColor = UIColor.xlChart.color(rgba: "#49ADFF") 79 | let bottomTextFourColor = UIColor.xlChart.color(rgba: "#DC66ED") 80 | 81 | let lineBorderColor = UIColor(red:1, green:1, blue:1, alpha:0.1) 82 | let crossLineColor = UIColor(red:1, green:1, blue:1, alpha:0.6) 83 | let textColor = UIColor(red:1, green:1, blue:1, alpha:0.4) 84 | 85 | var riseColor: UIColor { 86 | // if let REB_GREEN = UserDefaults.standard.value(forKey: "REB_GREEN") as? NSNumber { 87 | // return REB_GREEN.isEqual(to: NSNumber.init(value: 0)) ? UIColor.xlChart.color(rgba: BHCOLORSTRING_CH_2) : UIColor.xlChart.color(rgba: BHCOLORSTRING_CH_1) 88 | // } 89 | return UIColor.xlChart.color(rgba: "#00C087") // 涨 默认green 90 | } 91 | 92 | var fallColor: UIColor { 93 | // if let REB_GREEN = UserDefaults.standard.value(forKey: "REB_GREEN") as? NSNumber { 94 | // return REB_GREEN.isEqual(to: NSNumber.init(value: 0)) ? UIColor.xlChart.color(rgba: BHCOLORSTRING_CH_1) : UIColor.xlChart.color(rgba: BHCOLORSTRING_CH_2) 95 | // } 96 | return UIColor.xlChart.color(rgba: "#FE6D39") // 跌 默认red 97 | } 98 | 99 | // 折线颜色 100 | let minLineColor = UIColor.xlChart.color(rgba: "#2CA0FF") 101 | let minLineMaColor = UIColor.xlChart.color(rgba: "#ffc004") 102 | let minLinefillColor = UIColor(red: 0, green: 122.0/255.0, blue: 1, alpha: 0.08) 103 | 104 | // Bar柱状图 上下间隙 105 | let barChartMinYGap: CGFloat = 13 106 | // Bar柱状图 左右两边间隙 107 | let barChartMinXGap: CGFloat = 4 108 | let barWidth: CGFloat = 25 109 | // Bar柱状图最小高度 110 | var minBarHeight: CGFloat = 1 111 | let barLineBorderColor = UIColor(red: 0, green: 71.0/255.0, blue: 148.0/255.0, alpha: 0.1) 112 | 113 | 114 | /// 十字线文字边框颜色 115 | let crossBorderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.6).cgColor 116 | 117 | /// 十字线文字背景颜色 118 | let crossBgColor = UIColor.xlChart.color(rgba: "#1F2D3F") 119 | 120 | let crossTextLayerHeight: CGFloat = 24 121 | 122 | let highLowBgColor = UIColor.clear 123 | 124 | let baseFont = UIFont.systemFont(ofSize: 10) 125 | 126 | func getTextSize(text: String) -> CGSize { 127 | 128 | let size = text.size(withAttributes: [NSAttributedString.Key.font: baseFont]) 129 | let width = ceil(size.width) + 5 130 | let height = ceil(size.height) 131 | 132 | return CGSize(width: width, height: height) 133 | } 134 | 135 | func getCrossTextSize(text: String) -> CGSize { 136 | 137 | let size = text.size(withAttributes: [NSAttributedString.Key.font: baseFont]) 138 | let width = ceil(size.width) + 12 139 | let height = ceil(size.height) + 12 140 | 141 | return CGSize(width: width, height: height) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/Helpers/XLStockChartNameSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLStockChartNameSpace.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | 12 | public protocol NameSpaceProtocol { 13 | associatedtype WrapperType 14 | var xlChart: WrapperType { get } 15 | static var xlChart: WrapperType.Type { get } 16 | } 17 | 18 | public extension NameSpaceProtocol { 19 | var xlChart: NameSpaceWrapper { 20 | return NameSpaceWrapper(value: self) 21 | } 22 | 23 | static var xlChart: NameSpaceWrapper.Type { 24 | return NameSpaceWrapper.self 25 | } 26 | } 27 | 28 | public struct NameSpaceWrapper { 29 | public let wrappedValue: T 30 | public init(value: T) { 31 | self.wrappedValue = value 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLBottomChartTextLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLBottomChartTextLayer.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/23. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// K线底部 例:成交量 VOL5 VOL10 文字layer 12 | class XLBottomChartTextLayer: XLCAShapeLayer, XLDrawLayerProtocol { 13 | 14 | var theme = XLKLineStyle() 15 | 16 | fileprivate let textHeight: CGFloat = 12 17 | fileprivate let textTop: CGFloat = 1 18 | 19 | override init() { 20 | super.init() 21 | addSublayer(oneText) 22 | addSublayer(twoText) 23 | addSublayer(threeText) 24 | addSublayer(fourText) 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | func configureBottomValue(secondDrawString: String, one: CGFloat, two: CGFloat, three: CGFloat) { 32 | 33 | if secondDrawString == KLINEVOL { 34 | // VOL 35 | let oneString = "成交量: " + (one > 0 ? one.xlChart.kLineVolNumber() : "--") 36 | let twoString = "MA5: " + (two > 0 ? two.xlChart.kLineVolNumber() : "--") 37 | let threeString = "MA10: " + (three > 0 ? three.xlChart.kLineVolNumber() : "--") 38 | 39 | let oneSize = getTextSize(text: oneString) 40 | let twoSize = getTextSize(text: twoString) 41 | let threeSize = getTextSize(text: threeString) 42 | 43 | oneText.string = oneString 44 | twoText.string = twoString 45 | threeText.string = threeString 46 | 47 | oneText.frame = CGRect(x: 4, y: 0, width: oneSize.width, height: self.theme.midTextHeight) 48 | 49 | var twoHeight: CGFloat = 0 50 | var twoTop: CGFloat = 0 51 | if two > 10000 { 52 | twoHeight = self.theme.midTextHeight 53 | twoTop = 0 54 | }else { 55 | twoHeight = self.theme.midTextHeight - 1 56 | twoTop = textTop 57 | } 58 | twoText.frame = CGRect(x: self.oneText.frame.maxX, y: twoTop, width: twoSize.width, height: twoHeight) 59 | 60 | var threeHeight: CGFloat = 0 61 | var threeTop: CGFloat = 0 62 | if three > 10000 { 63 | threeHeight = self.theme.midTextHeight 64 | threeTop = 0 65 | }else { 66 | threeHeight = self.theme.midTextHeight - 1 67 | threeTop = textTop 68 | } 69 | threeText.frame = CGRect(x: self.twoText.frame.maxX, y: threeTop, width: threeSize.width, height: threeHeight) 70 | 71 | oneText.isHidden = false 72 | twoText.isHidden = false 73 | threeText.isHidden = false 74 | fourText.isHidden = true 75 | }else if secondDrawString == KLINEMACD { 76 | // MACD 77 | let oneString = "MACD(12,26,9) " 78 | let twoString = "MACD: " + one.xlChart.kLineVolNumber() 79 | let threeString = "DIF: " + two.xlChart.kLineVolNumber() 80 | let fourString = "DEA: " + three.xlChart.kLineVolNumber() 81 | 82 | let oneWidth = getTextSize(text: oneString).width 83 | let twoWidth = getTextSize(text: twoString).width 84 | let threeWidth = getTextSize(text: threeString).width 85 | let fourWidth = getTextSize(text: fourString).width 86 | 87 | oneText.string = oneString 88 | twoText.string = twoString 89 | threeText.string = threeString 90 | fourText.string = fourString 91 | 92 | oneText.frame = CGRect(x: 4, y: textTop, width: oneWidth, height: self.theme.midTextHeight - textTop) 93 | twoText.frame = CGRect(x: self.oneText.frame.maxX, y: textTop, width: twoWidth, height: self.theme.midTextHeight - textTop) 94 | threeText.frame = CGRect(x: self.twoText.frame.maxX, y: textTop, width: threeWidth, height: self.theme.midTextHeight - textTop) 95 | fourText.frame = CGRect(x: self.threeText.frame.maxX, y: textTop, width: fourWidth, height: self.theme.midTextHeight - textTop) 96 | 97 | oneText.isHidden = false 98 | twoText.isHidden = false 99 | threeText.isHidden = false 100 | fourText.isHidden = false 101 | }else if secondDrawString == KLINEKDJ { 102 | // KDJ 103 | let oneString = "KDJ(14,1,3) " 104 | let twoString = "K: " + one.xlChart.kLineVolNumber() 105 | let threeString = "D: " + two.xlChart.kLineVolNumber() 106 | let fourString = "J: " + three.xlChart.kLineVolNumber() 107 | 108 | let oneWidth = getTextSize(text: oneString).width 109 | let twoWidth = getTextSize(text: twoString).width 110 | let threeWidth = getTextSize(text: threeString).width 111 | let fourWidth = getTextSize(text: fourString).width 112 | 113 | oneText.string = oneString 114 | twoText.string = twoString 115 | threeText.string = threeString 116 | fourText.string = fourString 117 | 118 | oneText.frame = CGRect(x: 4, y: textTop, width: oneWidth, height: self.theme.midTextHeight - textTop) 119 | twoText.frame = CGRect(x: self.oneText.frame.maxX, y: textTop, width: twoWidth, height: self.theme.midTextHeight - textTop) 120 | threeText.frame = CGRect(x: self.twoText.frame.maxX, y: textTop, width: threeWidth, height: self.theme.midTextHeight - textTop) 121 | fourText.frame = CGRect(x: self.threeText.frame.maxX, y: textTop, width: fourWidth, height: self.theme.midTextHeight - textTop) 122 | 123 | oneText.isHidden = false 124 | twoText.isHidden = false 125 | threeText.isHidden = false 126 | fourText.isHidden = false 127 | 128 | }else if secondDrawString == KLINERSI { 129 | // RSI 130 | let twoString = "RSI(14): " + one.xlChart.kLineVolNumber() 131 | let twoWidth = getTextSize(text: twoString).width 132 | twoText.string = twoString 133 | 134 | twoText.frame = CGRect(x: 4, y: textTop, width: twoWidth, height: self.theme.midTextHeight - textTop) 135 | twoText.isHidden = false 136 | 137 | oneText.isHidden = true 138 | threeText.isHidden = true 139 | fourText.isHidden = true 140 | } 141 | } 142 | 143 | // MARK: - Lazy 144 | lazy var oneText: XLCATextLayer = { 145 | let oneText = XLCATextLayer() 146 | oneText.fontSize = 10 147 | oneText.foregroundColor = self.theme.bottomTextOneColor.cgColor 148 | oneText.backgroundColor = UIColor.clear.cgColor 149 | oneText.alignmentMode = CATextLayerAlignmentMode.left 150 | oneText.contentsScale = UIScreen.main.scale 151 | return oneText 152 | }() 153 | 154 | lazy var twoText: XLCATextLayer = { 155 | let twoText = XLCATextLayer() 156 | twoText.fontSize = 10 157 | twoText.foregroundColor = self.theme.bottomTextTwoColor.cgColor 158 | twoText.backgroundColor = UIColor.clear.cgColor 159 | twoText.alignmentMode = CATextLayerAlignmentMode.left 160 | twoText.contentsScale = UIScreen.main.scale 161 | return twoText 162 | }() 163 | 164 | lazy var threeText: XLCATextLayer = { 165 | let threeText = XLCATextLayer() 166 | threeText.fontSize = 10 167 | threeText.foregroundColor = self.theme.bottomTextThreeColor.cgColor 168 | threeText.backgroundColor = UIColor.clear.cgColor 169 | threeText.alignmentMode = CATextLayerAlignmentMode.left 170 | threeText.contentsScale = UIScreen.main.scale 171 | return threeText 172 | }() 173 | 174 | lazy var fourText: XLCATextLayer = { 175 | let fourText = XLCATextLayer() 176 | fourText.fontSize = 10 177 | fourText.foregroundColor = self.theme.bottomTextFourColor.cgColor 178 | fourText.backgroundColor = UIColor.clear.cgColor 179 | fourText.alignmentMode = CATextLayerAlignmentMode.left 180 | fourText.contentsScale = UIScreen.main.scale 181 | return fourText 182 | }() 183 | } 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLCrossLineLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLCrossLineLayer.swift 3 | // behoo 4 | // 5 | // Created by 夏磊 on 2018/8/24. 6 | // Copyright © 2018年 behoo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 十字线layer 12 | class XLCrossLineLayer: CAShapeLayer { 13 | 14 | var theme = XLKLineStyle() 15 | 16 | var topChartHeight: CGFloat { 17 | get { 18 | return frame.height - theme.topTextHeight - theme.midTextHeight - theme.bottomChartHeight - theme.timeTextHeight 19 | } 20 | } 21 | 22 | /// 十字线 23 | lazy var corssLineLayer: XLCAShapeLayer = { 24 | let line = XLCAShapeLayer() 25 | line.lineWidth = self.theme.corssLineWidth 26 | line.strokeColor = UIColor(red:1, green:1, blue:1, alpha:0.6).cgColor 27 | line.fillColor = UIColor(red:1, green:1, blue:1, alpha:0.6).cgColor 28 | return line 29 | }() 30 | 31 | /// Y轴标签 32 | lazy var yMarkLayer: XLCAShapeLayer = { 33 | let yMarkLayer = XLCAShapeLayer() 34 | yMarkLayer.backgroundColor = self.theme.crossBgColor.cgColor 35 | yMarkLayer.borderColor = self.theme.crossBorderColor 36 | yMarkLayer.borderWidth = self.theme.frameWidth 37 | yMarkLayer.cornerRadius = 2 38 | yMarkLayer.contentsScale = UIScreen.main.scale 39 | 40 | yMarkLayer.addSublayer(self.yMarkTextLayer) 41 | return yMarkLayer 42 | }() 43 | 44 | /// Y轴文字 45 | lazy var yMarkTextLayer: XLCATextLayer = { 46 | let yMarkTextLayer = XLCATextLayer() 47 | yMarkTextLayer.fontSize = 10 48 | yMarkTextLayer.foregroundColor = UIColor.white.cgColor 49 | yMarkTextLayer.backgroundColor = UIColor.clear.cgColor 50 | yMarkTextLayer.alignmentMode = .center 51 | yMarkTextLayer.contentsScale = UIScreen.main.scale 52 | return yMarkTextLayer 53 | }() 54 | 55 | /// time标签 56 | lazy var timeMarkLayer: XLCAShapeLayer = { 57 | let timeMarkLayer = XLCAShapeLayer() 58 | timeMarkLayer.backgroundColor = self.theme.crossBgColor.cgColor 59 | timeMarkLayer.borderColor = self.theme.crossBorderColor 60 | timeMarkLayer.borderWidth = self.theme.frameWidth 61 | timeMarkLayer.cornerRadius = 2 62 | timeMarkLayer.contentsScale = UIScreen.main.scale 63 | 64 | timeMarkLayer.addSublayer(self.timeMarkTextLayer) 65 | return timeMarkLayer 66 | }() 67 | 68 | /// time文字 69 | lazy var timeMarkTextLayer: XLCATextLayer = { 70 | let timeMarkTextLayer = XLCATextLayer() 71 | timeMarkTextLayer.fontSize = 10 72 | timeMarkTextLayer.foregroundColor = UIColor.white.cgColor 73 | timeMarkTextLayer.backgroundColor = UIColor.clear.cgColor 74 | timeMarkTextLayer.alignmentMode = .center 75 | timeMarkTextLayer.contentsScale = UIScreen.main.scale 76 | return timeMarkTextLayer 77 | }() 78 | 79 | /// 中心圆点 80 | lazy var circleBigLayer: XLCAShapeLayer = { 81 | let circleBigLayer = XLCAShapeLayer() 82 | circleBigLayer.cornerRadius = self.theme.crossCircleWidth * 0.5 83 | circleBigLayer.backgroundColor = UIColor.white.cgColor 84 | return circleBigLayer 85 | }() 86 | 87 | lazy var circleMidLayer: XLCAShapeLayer = { 88 | let circleMidLayer = XLCAShapeLayer() 89 | circleMidLayer.cornerRadius = (self.theme.crossCircleWidth - 2) * 0.5 90 | circleMidLayer.backgroundColor = UIColor.xlChart.color(rgba: "#243245").cgColor 91 | return circleMidLayer 92 | }() 93 | 94 | lazy var circleSmallLayer: XLCAShapeLayer = { 95 | let circleSmallLayer = XLCAShapeLayer() 96 | circleSmallLayer.cornerRadius = (self.theme.crossCircleWidth - 4) * 0.5 97 | circleSmallLayer.backgroundColor = UIColor.white.cgColor 98 | return circleSmallLayer 99 | }() 100 | 101 | override init() { 102 | super.init() 103 | 104 | isHidden = true 105 | addSublayer(corssLineLayer) 106 | addSublayer(circleBigLayer) 107 | addSublayer(circleMidLayer) 108 | addSublayer(circleSmallLayer) 109 | addSublayer(yMarkLayer) 110 | addSublayer(timeMarkLayer) 111 | } 112 | 113 | required init?(coder aDecoder: NSCoder) { 114 | fatalError("init(coder:) has not been implemented") 115 | } 116 | 117 | public func moveCrossLineLayer(touchNum: CGFloat?, touchPoint: CGPoint, pricePoint: CGPoint, volumePoint: CGPoint, model: XLKLineModel?, secondString: String?, dateType: XLKLineDateType) { 118 | 119 | guard let model = model else { return } 120 | 121 | let linePath = UIBezierPath() 122 | 123 | // 竖线 124 | linePath.move(to: CGPoint(x: pricePoint.x, y: theme.topTextHeight)) 125 | linePath.addLine(to: CGPoint(x: pricePoint.x, y: theme.topTextHeight + topChartHeight)) 126 | 127 | linePath.move(to: CGPoint(x: pricePoint.x, y: theme.topTextHeight + topChartHeight + theme.midTextHeight)) 128 | linePath.addLine(to: CGPoint(x: pricePoint.x, y: frame.height - theme.timeTextHeight)) 129 | 130 | let xlineTop = touchPoint.y 131 | 132 | // 横线 133 | if touchNum != nil { 134 | linePath.move(to: CGPoint(x: 0, y: xlineTop)) 135 | linePath.addLine(to: CGPoint(x: frame.width, y: xlineTop)) 136 | 137 | // 圆 138 | var circleWidth: CGFloat = theme.crossCircleWidth 139 | circleBigLayer.frame = CGRect(x: pricePoint.x - circleWidth*0.5, y: xlineTop - circleWidth*0.5, width: circleWidth, height: circleWidth) 140 | 141 | circleWidth = circleWidth - 2 142 | circleMidLayer.frame = CGRect(x: pricePoint.x - circleWidth*0.5, y: xlineTop - circleWidth*0.5, width: circleWidth, height: circleWidth) 143 | 144 | circleWidth = circleWidth - 2 145 | circleSmallLayer.frame = CGRect(x: pricePoint.x - circleWidth*0.5, y: xlineTop - circleWidth*0.5, width: circleWidth, height: circleWidth) 146 | 147 | circleBigLayer.isHidden = false 148 | circleMidLayer.isHidden = false 149 | circleSmallLayer.isHidden = false 150 | }else { 151 | circleBigLayer.isHidden = true 152 | circleMidLayer.isHidden = true 153 | circleSmallLayer.isHidden = true 154 | } 155 | 156 | corssLineLayer.path = linePath.cgPath 157 | 158 | // Y轴标签 159 | var labelX: CGFloat = 0 160 | var labelY: CGFloat = 0 161 | 162 | if let touchNum = touchNum { 163 | var yMarkString = "" 164 | if secondString == KLINEVOL { 165 | yMarkString = touchNum.xlChart.kLineVolNumber() 166 | }else { 167 | yMarkString = touchNum.xlChart.kLinePriceNumber() 168 | } 169 | let yMarkStringSize = theme.getCrossTextSize(text: yMarkString) 170 | labelX = 0 + 2 171 | labelY = xlineTop - yMarkStringSize.height * 0.5 172 | if labelY <= theme.topTextHeight { 173 | labelY = theme.topTextHeight 174 | } 175 | let maxY = frame.height - theme.timeTextHeight - yMarkStringSize.height 176 | 177 | if labelY >= maxY { 178 | labelY = maxY 179 | } 180 | yMarkTextLayer.string = yMarkString 181 | yMarkTextLayer.frame = CGRect(x: 6, y: 6, width: yMarkStringSize.width - 12, height: yMarkStringSize.height - 12) 182 | yMarkLayer.frame = CGRect(x: labelX, y: labelY, width: yMarkStringSize.width, height: yMarkStringSize.height) 183 | yMarkLayer.isHidden = false 184 | }else { 185 | yMarkLayer.isHidden = true 186 | } 187 | 188 | // time标签 189 | var timeMarkString = "" 190 | if dateType == .min { 191 | timeMarkString = model.time.xlChart.toTimeString("MM/dd HH:mm") 192 | }else { 193 | timeMarkString = model.time.xlChart.toTimeString("YY/MM/dd") 194 | } 195 | let timeMarkStringSize = theme.getCrossTextSize(text: timeMarkString) 196 | 197 | let maxX = UIScreen.main.bounds.width - timeMarkStringSize.width 198 | labelX = pricePoint.x - timeMarkStringSize.width * 0.5 199 | labelY = frame.height - theme.timeTextHeight 200 | if labelX > maxX { 201 | labelX = maxX 202 | } else if labelX < 0 { 203 | labelX = 0 204 | } 205 | timeMarkTextLayer.string = timeMarkString 206 | timeMarkTextLayer.frame = CGRect(x: 6, y: 5.5, width: timeMarkStringSize.width - 12, height: timeMarkStringSize.height - 12) 207 | timeMarkLayer.frame = CGRect(x: labelX, y: labelY, width: timeMarkStringSize.width, height: timeMarkStringSize.height) 208 | 209 | isHidden = false 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLHighLowTextLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLHighLowTextLayer.swift 3 | // behoo 4 | // 5 | // Created by 夏磊 on 2018/9/3. 6 | // Copyright © 2018年 behoo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 最高最低文字layer 12 | class XLHighLowTextLayer: CAShapeLayer { 13 | 14 | var theme = XLKLineStyle() 15 | 16 | let lineLayerWidth: CGFloat = 15 17 | 18 | // 最高最低值靠近左侧屏幕最大偏移值 19 | let maxOffsetWidth: CGFloat = 60 20 | 21 | override init() { 22 | super.init() 23 | addSublayer(highTextLayer) 24 | addSublayer(highLineLayer) 25 | addSublayer(lowTextLayer) 26 | addSublayer(lowLineLayer) 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | public func moveHighLowLayer(highPoint: CGPoint, highPrice: CGFloat, lowPoint: CGPoint, lowPrice: CGFloat, startX: CGFloat) { 34 | 35 | var labelX: CGFloat = 0 36 | var labelY: CGFloat = 0 37 | 38 | if highPoint == CGPoint.zero { 39 | highTextLayer.isHidden = true 40 | highLineLayer.isHidden = true 41 | }else { 42 | let linePath = UIBezierPath() 43 | linePath.move(to: highPoint) 44 | 45 | let highMarkString = highPrice.xlChart.kLinePriceNumber() 46 | let highMarkStringSize = theme.getTextSize(text: highMarkString) 47 | 48 | if highPoint.x - startX > frame.width * 0.5 { 49 | // label在左侧 50 | linePath.addLine(to: CGPoint(x: highPoint.x - lineLayerWidth, y: highPoint.y)) 51 | labelX = highPoint.x - highMarkStringSize.width - lineLayerWidth 52 | 53 | } else { 54 | // label在右侧 55 | // 如果是在左侧屏幕边缘 往右边拉长 56 | var offset:CGFloat = 0 57 | if highPoint.x - startX < maxOffsetWidth { 58 | offset = maxOffsetWidth - (highPoint.x - startX) 59 | } 60 | 61 | linePath.addLine(to: CGPoint(x: highPoint.x + lineLayerWidth + offset, y: highPoint.y)) 62 | labelX = highPoint.x + lineLayerWidth + offset 63 | } 64 | labelY = highPoint.y - highMarkStringSize.height * 0.5 65 | highTextLayer.string = highMarkString 66 | highTextLayer.frame = CGRect(x: labelX, y: labelY, width: highMarkStringSize.width, height: highMarkStringSize.height) 67 | highTextLayer.isHidden = false 68 | 69 | highLineLayer.path = linePath.cgPath 70 | highLineLayer.isHidden = false 71 | } 72 | 73 | if lowPoint == CGPoint.zero { 74 | lowTextLayer.isHidden = true 75 | lowLineLayer.isHidden = true 76 | }else { 77 | let linePath = UIBezierPath() 78 | linePath.move(to: lowPoint) 79 | 80 | let lowMarkString = lowPrice.xlChart.kLinePriceNumber() 81 | let lowMarkStringSize = theme.getTextSize(text: lowMarkString) 82 | 83 | if lowPoint.x - startX > frame.width * 0.5 { 84 | // label在左侧 85 | labelX = lowPoint.x - lowMarkStringSize.width - lineLayerWidth 86 | 87 | linePath.addLine(to: CGPoint(x: lowPoint.x - lineLayerWidth, y: lowPoint.y)) 88 | } else { 89 | // label在右侧 90 | // 如果是在左侧屏幕边缘 往右边拉长 91 | var offset:CGFloat = 0 92 | if lowPoint.x - startX < maxOffsetWidth { 93 | offset = maxOffsetWidth - (lowPoint.x - startX) 94 | } 95 | 96 | linePath.addLine(to: CGPoint(x: lowPoint.x + lineLayerWidth + offset, y: lowPoint.y)) 97 | labelX = lowPoint.x + lineLayerWidth + offset 98 | } 99 | labelY = lowPoint.y - lowMarkStringSize.height * 0.5 100 | lowTextLayer.string = lowMarkString 101 | lowTextLayer.frame = CGRect(x: labelX, y: labelY, width: lowMarkStringSize.width, height: lowMarkStringSize.height) 102 | lowTextLayer.isHidden = false 103 | 104 | lowLineLayer.path = linePath.cgPath 105 | lowLineLayer.isHidden = false 106 | } 107 | } 108 | 109 | // MARK: - Lazy 110 | /// 最高 111 | lazy var highTextLayer: XLCATextLayer = { 112 | let highTextLayer = XLCATextLayer() 113 | highTextLayer.fontSize = 10 114 | highTextLayer.foregroundColor = UIColor(red:1, green:1, blue:1, alpha:0.8).cgColor 115 | highTextLayer.backgroundColor = self.theme.highLowBgColor.cgColor 116 | highTextLayer.alignmentMode = .center 117 | highTextLayer.contentsScale = UIScreen.main.scale 118 | highTextLayer.isHidden = true 119 | return highTextLayer 120 | }() 121 | 122 | /// 最高line 123 | lazy var highLineLayer: CAShapeLayer = { 124 | let line = CAShapeLayer() 125 | line.lineWidth = self.theme.highLowLineWidth 126 | line.strokeColor = UIColor.white.cgColor 127 | line.fillColor = UIColor.white.cgColor 128 | line.isHidden = true 129 | return line 130 | }() 131 | 132 | /// 最低 133 | lazy var lowTextLayer: XLCATextLayer = { 134 | let lowTextLayer = XLCATextLayer() 135 | lowTextLayer.fontSize = 10 136 | lowTextLayer.foregroundColor = UIColor(red:1, green:1, blue:1, alpha:0.8).cgColor 137 | lowTextLayer.backgroundColor = self.theme.highLowBgColor.cgColor 138 | lowTextLayer.alignmentMode = .center 139 | lowTextLayer.contentsScale = UIScreen.main.scale 140 | lowTextLayer.isHidden = true 141 | return lowTextLayer 142 | }() 143 | 144 | /// 最低line 145 | lazy var lowLineLayer: CAShapeLayer = { 146 | let line = CAShapeLayer() 147 | line.lineWidth = self.theme.highLowLineWidth 148 | line.strokeColor = UIColor.white.cgColor 149 | line.fillColor = UIColor.white.cgColor 150 | line.isHidden = true 151 | return line 152 | }() 153 | } 154 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLKLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLKLine.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class XLCAShapeLayer: CAShapeLayer { 12 | override open func action(forKey event: String) -> CAAction? { 13 | return nil 14 | } 15 | } 16 | 17 | open class XLCATextLayer: CATextLayer { 18 | override open func action(forKey event: String) -> CAAction? { 19 | return nil 20 | } 21 | } 22 | 23 | public class XLKLine: UIView, XLDrawLayerProtocol { 24 | 25 | public var theme = XLKLineStyle() 26 | 27 | var dataK: [XLKLineModel] = [] 28 | var positionModels: [XLKLineCoordModel] = [] 29 | var klineModels: [XLKLineModel] = [] 30 | 31 | var contentOffsetX: CGFloat = 0 32 | 33 | // 最高价 最低价 用于显示最高最低值 34 | var highestPrice: CGFloat = 0 35 | var highestPoint: CGPoint = CGPoint.zero 36 | var lowestPrice: CGFloat = 0 37 | var lowestPoint: CGPoint = CGPoint.zero 38 | 39 | // 横向分隔线的价格 根据坐标计算得来 40 | var onePrice: CGFloat = 0 41 | var twoPrice: CGFloat = 0 42 | var threePrice: CGFloat = 0 43 | var fourPrice: CGFloat = 0 44 | var fivePrice: CGFloat = 0 45 | 46 | // 主图 47 | var priceUnit: CGFloat = 0.01 48 | 49 | // 最大值 最小值 用于计算整个区间的范围 有可能需要算入BOLL的值 所以不能等同价格最大最小值 50 | var mainMaxPrice: CGFloat = 0 51 | var mainMinPrice: CGFloat = 0 52 | var mainMaxMA: CGFloat = 0 53 | var mainMinMA: CGFloat = 0 54 | var mainMaxBoll: CGFloat = 0 55 | var mainMinBoll: CGFloat = 0 56 | 57 | // 副图 58 | var bottomChartUnit: CGFloat = 0.01 59 | 60 | //VOL 61 | var maxVolume: CGFloat = 0 62 | var minVolume: CGFloat = 0 63 | 64 | // KDJ 65 | var maxKDJ: CGFloat = 0 66 | var minKDJ: CGFloat = 0 67 | 68 | // MACD 69 | var maxMACD: CGFloat = 0 70 | var minMACD: CGFloat = 0 71 | 72 | // RSI 73 | var maxRSI: CGFloat = 0 74 | var minRSI: CGFloat = 0 75 | 76 | var renderRect: CGRect = CGRect.zero 77 | var renderWidth: CGFloat = 0 78 | 79 | // 时间layer 80 | lazy var xAxisTimeMarkLayer = XLCAShapeLayer() 81 | 82 | // 主图layer 83 | lazy var minLineChartLayer = XLCAShapeLayer() 84 | lazy var minLineFillColorLayer = XLCAShapeLayer() 85 | lazy var ma60LineLayer = XLCAShapeLayer() 86 | // 蜡烛图 87 | lazy var candleChartLayer = XLCAShapeLayer() 88 | // MA 89 | lazy var ma5LineLayer = XLCAShapeLayer() 90 | lazy var ma10LineLayer = XLCAShapeLayer() 91 | lazy var ma30LineLayer = XLCAShapeLayer() 92 | // BOLL 93 | lazy var upBollLineLayer = XLCAShapeLayer() 94 | lazy var midBollLineLayer = XLCAShapeLayer() 95 | lazy var lowBollLineLayer = XLCAShapeLayer() 96 | 97 | // 副图layer 98 | // VOL 99 | lazy var volumeLayer = XLCAShapeLayer() 100 | lazy var volMa5LineLayer = XLCAShapeLayer() 101 | lazy var volMa10LineLayer = XLCAShapeLayer() 102 | 103 | // MACD 104 | lazy var macd_barLayer = XLCAShapeLayer() 105 | lazy var macd_diffLayer = XLCAShapeLayer() 106 | lazy var macd_deaLayer = XLCAShapeLayer() 107 | 108 | // KDJ 109 | lazy var kdj_K_LineLayer = XLCAShapeLayer() 110 | lazy var kdj_D_LineLayer = XLCAShapeLayer() 111 | lazy var kdj_J_LineLayer = XLCAShapeLayer() 112 | 113 | // RSI 114 | lazy var rsiLineLayer = XLCAShapeLayer() 115 | 116 | // 最高最低 117 | var highLowLayer: XLHighLowTextLayer! 118 | 119 | var mainDrawString: String = KLINEMA 120 | var secondDrawString: String = KLINEVOL 121 | 122 | var dateType: XLKLineDateType = .min 123 | var lineType: XLKLineType = .candleLineType 124 | 125 | var topChartHeight: CGFloat { 126 | get { 127 | return self.frame.maxY - theme.topTextHeight - theme.midTextHeight - theme.bottomChartHeight - theme.timeTextHeight 128 | } 129 | } 130 | 131 | var topChartTop: CGFloat { 132 | get { 133 | return theme.topTextHeight 134 | } 135 | } 136 | 137 | var midTextTop: CGFloat { 138 | get { 139 | return theme.topTextHeight + topChartHeight 140 | } 141 | } 142 | 143 | var bottomChartTop: CGFloat { 144 | get { 145 | return theme.topTextHeight + topChartHeight + theme.midTextHeight 146 | } 147 | } 148 | 149 | var timeTextTop: CGFloat { 150 | get { 151 | return theme.topTextHeight + topChartHeight + theme.midTextHeight + theme.bottomChartHeight 152 | } 153 | } 154 | 155 | // 计算处于当前显示区域左边隐藏的蜡烛图的个数,即为当前显示的初始 index 156 | var startIndex: Int { 157 | get { 158 | let scrollViewOffsetX = contentOffsetX < 0 ? 0 : contentOffsetX 159 | var leftCandleCount = Int(abs(scrollViewOffsetX) / (theme.candleWidth + theme.candleGap)) 160 | 161 | if leftCandleCount > dataK.count { 162 | leftCandleCount = dataK.count - 1 163 | return leftCandleCount 164 | } else if leftCandleCount == 0 { 165 | return leftCandleCount 166 | } else { 167 | return leftCandleCount + 1 168 | } 169 | } 170 | } 171 | 172 | // 当前显示区域起始横坐标 x 173 | var startX: CGFloat { 174 | get { 175 | let scrollViewOffsetX = contentOffsetX < 0 ? 0 : contentOffsetX 176 | return scrollViewOffsetX 177 | } 178 | } 179 | 180 | // 当前显示区域最多显示的蜡烛图个数 181 | var countOfshowCandle: Int { 182 | get { 183 | // return Int((renderWidth - theme.candleWidth) / ( theme.candleWidth + theme.candleGap)) 184 | return Int(renderWidth / ( theme.candleWidth + theme.candleGap)) 185 | } 186 | } 187 | 188 | // MARK: - Initialize 189 | override init(frame: CGRect) { 190 | super.init(frame: frame) 191 | self.backgroundColor = UIColor.clear 192 | 193 | highLowLayer = XLHighLowTextLayer() 194 | highLowLayer.frame = bounds 195 | self.layer.addSublayer(highLowLayer) 196 | } 197 | 198 | required public init?(coder aDecoder: NSCoder) { 199 | fatalError("init(coder:) has not been implemented") 200 | } 201 | 202 | 203 | // MARK: - Drawing Function 204 | func drawKLineView(mainDrawString: String, secondDrawString: String, lineType: XLKLineType) { 205 | self.mainDrawString = mainDrawString 206 | self.secondDrawString = secondDrawString 207 | self.lineType = lineType 208 | 209 | calcMaxAndMinData() 210 | convertToPositionModel(data: dataK) 211 | 212 | clearLayer() 213 | drawxAxisTimeMarkLayer() 214 | 215 | 216 | // 主图 217 | if lineType == .minLineType { 218 | // 分时 219 | drawMinLineChartLayer(array: positionModels) 220 | highLowLayer.isHidden = true 221 | 222 | if mainDrawString == KLINEMA { 223 | drawMinLineMALayer(array: positionModels) 224 | }else if mainDrawString == KLINEBOLL { 225 | drawMinLineBOLLLayer(array: positionModels) 226 | }else if mainDrawString == KLINEHIDE { 227 | print("隐藏") 228 | } 229 | }else { 230 | // K线 231 | drawCandleChartLayer(array: positionModels) 232 | if mainDrawString == KLINEMA { 233 | drawMainMALayer(array: positionModels) 234 | }else if mainDrawString == KLINEBOLL { 235 | drawBOLLLayer(array: positionModels) 236 | }else if mainDrawString == KLINEHIDE { 237 | print("隐藏") 238 | } 239 | 240 | // 最高最低 241 | drawHighLowLayer() 242 | highLowLayer.isHidden = false 243 | } 244 | 245 | // 副图 246 | if secondDrawString == KLINEVOL { 247 | drawVolumeLayer(array: positionModels) 248 | drawVolMALayer(array: positionModels) 249 | }else if secondDrawString == KLINEMACD { 250 | drawMACD_BAR_Layer(array: positionModels) 251 | drawMACD_DIF_DEA_Layer(array: positionModels) 252 | }else if secondDrawString == KLINEKDJ { 253 | drawKDJLayer(array: positionModels) 254 | }else if secondDrawString == KLINERSI { 255 | drawRsiLayer(array: positionModels) 256 | } 257 | } 258 | 259 | /// 计算当前显示区域的最大最小值 260 | fileprivate func calcMaxAndMinData() { 261 | if dataK.count > 0 { 262 | 263 | self.mainMaxPrice = CGFloat.leastNormalMagnitude 264 | self.mainMinPrice = CGFloat.greatestFiniteMagnitude 265 | self.mainMaxMA = CGFloat.leastNormalMagnitude 266 | self.mainMinMA = CGFloat.greatestFiniteMagnitude 267 | self.mainMaxBoll = CGFloat.leastNormalMagnitude 268 | self.mainMinBoll = CGFloat.greatestFiniteMagnitude 269 | 270 | self.maxVolume = CGFloat.leastNormalMagnitude 271 | self.minVolume = CGFloat.greatestFiniteMagnitude 272 | self.maxKDJ = -CGFloat.greatestFiniteMagnitude 273 | self.minKDJ = CGFloat.greatestFiniteMagnitude 274 | self.maxMACD = -CGFloat.greatestFiniteMagnitude 275 | self.minMACD = CGFloat.greatestFiniteMagnitude 276 | self.maxRSI = -CGFloat.greatestFiniteMagnitude 277 | self.minRSI = CGFloat.greatestFiniteMagnitude 278 | 279 | let startIndex = self.startIndex 280 | // 比计算出来的多加一个,是为了避免计算结果的取整导致少画 281 | let count = (startIndex + countOfshowCandle + 1) > dataK.count ? dataK.count : (startIndex + countOfshowCandle + 1) 282 | 283 | if startIndex < count { 284 | for i in startIndex ..< count { 285 | let entity = dataK[i] 286 | 287 | // 主图 288 | if self.lineType == .minLineType { 289 | // 分时 290 | self.mainMaxPrice = self.mainMaxPrice > entity.close ? self.mainMaxPrice : entity.close 291 | self.mainMinPrice = self.mainMinPrice < entity.close ? self.mainMinPrice : entity.close 292 | 293 | if self.mainDrawString == KLINEMA { 294 | // MA 295 | self.mainMaxMA = self.mainMaxMA > entity.ma60 ? self.mainMaxMA : entity.ma60 296 | 297 | // 过滤负数 298 | if entity.ma60 > 0 { 299 | self.mainMinMA = self.mainMinMA < entity.ma60 ? self.mainMinMA : entity.ma60 300 | } 301 | 302 | }else if self.mainDrawString == KLINEBOLL { 303 | // BOLL 304 | self.mainMaxBoll = self.mainMaxBoll > entity.boll_mb ? self.mainMaxBoll : entity.boll_mb 305 | self.mainMinBoll = self.mainMinBoll < entity.boll_mb ? self.mainMinBoll : entity.boll_mb 306 | } 307 | 308 | }else { 309 | // K线 310 | self.mainMaxPrice = self.mainMaxPrice > entity.high ? self.mainMaxPrice : entity.high 311 | self.mainMinPrice = self.mainMinPrice < entity.low ? self.mainMinPrice : entity.low 312 | 313 | if self.mainDrawString == KLINEMA { 314 | // MA 315 | let tempMAMax = max(entity.ma5, entity.ma10, entity.ma30) 316 | self.mainMaxMA = self.mainMaxMA > tempMAMax ? self.mainMaxMA : tempMAMax 317 | 318 | // 过滤负数 319 | if entity.ma5 > 0, entity.ma10 > 10, entity.ma30 > 0 { 320 | let tempMAMin = min(entity.ma5, entity.ma10, entity.ma30) 321 | self.mainMinMA = self.mainMinMA < tempMAMin ? self.mainMinMA : tempMAMin 322 | } 323 | 324 | }else if self.mainDrawString == KLINEBOLL { 325 | // BOLL 326 | self.mainMaxBoll = self.mainMaxBoll > entity.boll_up ? self.mainMaxBoll : entity.boll_up 327 | self.mainMinBoll = self.mainMinBoll < entity.boll_dn ? self.mainMinBoll : entity.boll_dn 328 | } 329 | } 330 | 331 | // 副图 332 | if self.secondDrawString == KLINEVOL { 333 | // VOL 334 | self.maxVolume = self.maxVolume > entity.volumefrom ? self.maxVolume : entity.volumefrom 335 | self.minVolume = self.minVolume < entity.volumefrom ? self.minVolume : entity.volumefrom 336 | 337 | 338 | // 计算VOL的MA5 MA10 339 | entity.volMa5 = calcVolMa(num: 5, targetIndex: i, isMustHasNum: true, dataK: dataK) ?? 0 340 | entity.volMa10 = calcVolMa(num: 10, targetIndex: i, isMustHasNum: true, dataK: dataK) ?? 0 341 | dataK[i] = entity 342 | 343 | self.maxVolume = max(entity.volMa5, entity.volMa10, self.maxVolume) 344 | 345 | if entity.volMa5 > 0, entity.volMa10 > 0 { 346 | self.minVolume = min(entity.volMa5, entity.volMa10, self.minVolume) 347 | } 348 | 349 | }else if self.secondDrawString == KLINEMACD { 350 | // MACD 351 | let tempMACDMax = max(entity.macd_bar, entity.macd_dea, entity.macd_diff) 352 | self.maxMACD = self.maxMACD > tempMACDMax ? self.maxMACD : tempMACDMax 353 | 354 | let tempMACDMin = min(entity.macd_bar, entity.macd_dea, entity.macd_diff) 355 | self.minMACD = self.minMACD < tempMACDMin ? self.minMACD : tempMACDMin 356 | 357 | 358 | }else if self.secondDrawString == KLINEKDJ { 359 | // KDJ 360 | let tempKDJMax = max(entity.kdj_k, entity.kdj_d, entity.kdj_j) 361 | self.maxKDJ = self.maxKDJ > tempKDJMax ? self.maxKDJ : tempKDJMax 362 | 363 | let tempKDJMin = min(entity.kdj_k, entity.kdj_d, entity.kdj_j) 364 | self.minKDJ = self.minKDJ < tempKDJMin ? self.minKDJ : tempKDJMin 365 | 366 | }else if self.secondDrawString == KLINERSI { 367 | // RSI 368 | self.maxRSI = self.maxRSI > entity.rsi ? self.maxRSI : entity.rsi 369 | 370 | self.minRSI = self.minRSI < entity.rsi ? self.minRSI : entity.rsi 371 | } 372 | } 373 | } 374 | 375 | // 算出需要绘制的最大最小值 376 | // 主图 377 | if self.mainDrawString == KLINEMA { 378 | // MA 379 | self.mainMaxPrice = self.mainMaxPrice > self.mainMaxMA ? self.mainMaxPrice : self.mainMaxMA 380 | self.mainMinPrice = self.mainMinPrice < self.mainMinMA ? self.mainMinPrice : self.mainMinMA 381 | }else if self.mainDrawString == KLINEBOLL { 382 | // BOLL 383 | self.mainMaxPrice = self.mainMaxPrice > self.mainMaxBoll ? self.mainMaxPrice : self.mainMaxBoll 384 | self.mainMinPrice = self.mainMinPrice < self.mainMinBoll ? self.mainMinPrice : self.mainMinBoll 385 | } 386 | 387 | // 副图 388 | if self.secondDrawString == KLINEVOL { 389 | // VOL 390 | }else if self.secondDrawString == KLINEMACD { 391 | // MACD 392 | }else if self.secondDrawString == KLINEKDJ { 393 | // KDJ 394 | }else if self.secondDrawString == KLINERSI { 395 | // RSI 396 | } 397 | } 398 | } 399 | 400 | /// 转换为坐标 model 401 | fileprivate func convertToPositionModel(data: [XLKLineModel]) { 402 | 403 | if data.count < 1 { 404 | return 405 | } 406 | 407 | self.positionModels.removeAll() 408 | self.klineModels.removeAll() 409 | 410 | let axisGap = countOfshowCandle / 4 411 | let gap = theme.topChartMinYGap 412 | let minY = gap 413 | 414 | let maxDiffPrice = self.mainMaxPrice - self.mainMinPrice 415 | if maxDiffPrice > 0 { 416 | priceUnit = (topChartHeight - 2 * minY) / maxDiffPrice 417 | } 418 | 419 | var maxDiffVolume: CGFloat = 0 420 | if self.secondDrawString == KLINEVOL { 421 | maxDiffVolume = self.maxVolume - self.minVolume 422 | bottomChartUnit = (theme.bottomChartHeight - theme.volumeGap) / maxDiffVolume 423 | }else if self.secondDrawString == KLINEMACD { 424 | maxDiffVolume = self.maxMACD - self.minMACD 425 | bottomChartUnit = (theme.bottomChartHeight - 2 * theme.volumeGap) / maxDiffVolume 426 | }else if self.secondDrawString == KLINEKDJ { 427 | maxDiffVolume = self.maxKDJ - self.minKDJ 428 | bottomChartUnit = (theme.bottomChartHeight - 2 * theme.volumeGap) / maxDiffVolume 429 | }else if self.secondDrawString == KLINERSI { 430 | maxDiffVolume = self.maxRSI - self.minRSI 431 | bottomChartUnit = (theme.bottomChartHeight - 2 * theme.volumeGap) / maxDiffVolume 432 | } 433 | 434 | let count = (startIndex + countOfshowCandle + 1) > data.count ? data.count : (startIndex + countOfshowCandle + 1) 435 | 436 | // 绘制顶部chart视图top顶部起始位置 437 | let topChartPostionTop = minY + topChartTop 438 | 439 | // 绘制底部chart视图top顶部起始位置 440 | let bottomChartPostionTop = bottomChartTop + theme.volumeGap 441 | 442 | // macdBar中线起始位置 443 | let startMacdBarY = abs(self.maxMACD) * bottomChartUnit + bottomChartPostionTop 444 | 445 | self.highestPrice = CGFloat.leastNormalMagnitude 446 | self.lowestPrice = CGFloat.greatestFiniteMagnitude 447 | 448 | // 计算几条横隔线的y值price 449 | let gapPrice: CGFloat = theme.topChartMinYGap / priceUnit 450 | self.onePrice = self.mainMaxPrice + gapPrice 451 | self.fivePrice = self.mainMinPrice - gapPrice 452 | let marginPrice = (self.onePrice - self.fivePrice) / 4.0 453 | self.fourPrice = self.fivePrice + marginPrice 454 | self.threePrice = self.fivePrice + marginPrice * 2 455 | self.twoPrice = self.fivePrice + marginPrice * 3 456 | 457 | if startIndex < count { 458 | for index in startIndex ..< count { 459 | let model = data[index] 460 | 461 | // 临时数据 462 | var highPoint: CGPoint = CGPoint.zero 463 | var lowPoint: CGPoint = CGPoint.zero 464 | var openPointY: CGFloat = 0 465 | var closePointY: CGFloat = 0 466 | var fillCandleColor = UIColor.black 467 | var candleRect = CGRect.zero 468 | 469 | var ma5Point: CGPoint = CGPoint.zero 470 | var ma10Point: CGPoint = CGPoint.zero 471 | var ma30Point: CGPoint = CGPoint.zero 472 | 473 | var ma60Point: CGPoint = CGPoint.zero 474 | 475 | var upBollPoint: CGPoint = CGPoint.zero 476 | var midBollPoint: CGPoint = CGPoint.zero 477 | var lowBollPoint: CGPoint = CGPoint.zero 478 | 479 | var minLinePoint: CGPoint = CGPoint.zero 480 | var minLine60MaPoint: CGPoint = CGPoint.zero 481 | 482 | var volumeStartPoint: CGPoint = CGPoint.zero 483 | var volumeEndPoint: CGPoint = CGPoint.zero 484 | var volMa5Point: CGPoint = CGPoint.zero 485 | var volMa10Point: CGPoint = CGPoint.zero 486 | 487 | var kdj_K_Point: CGPoint = CGPoint.zero 488 | var kdj_D_Point: CGPoint = CGPoint.zero 489 | var kdj_J_Point: CGPoint = CGPoint.zero 490 | 491 | var rsiPoint: CGPoint = CGPoint.zero 492 | 493 | var macdStartPoint: CGPoint = CGPoint.zero 494 | var macdEndPoint: CGPoint = CGPoint.zero 495 | var diffPoint: CGPoint = CGPoint.zero 496 | var deaPoint: CGPoint = CGPoint.zero 497 | var macdBarColor = UIColor.black 498 | 499 | // 公共数据 500 | let leftPosition = startX + CGFloat(index - startIndex) * (theme.candleWidth + theme.candleGap) 501 | let xPosition = leftPosition + theme.candleWidth * 0.5 502 | 503 | highPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.high) * priceUnit + topChartPostionTop) 504 | lowPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.low) * priceUnit + topChartPostionTop) 505 | openPointY = (mainMaxPrice - model.open) * priceUnit + topChartPostionTop 506 | closePointY = (mainMaxPrice - model.close) * priceUnit + topChartPostionTop 507 | 508 | // 蜡烛rect和颜色 509 | if(openPointY > closePointY) { 510 | fillCandleColor = theme.riseColor 511 | candleRect = CGRect(x: leftPosition, y: closePointY, width: theme.candleWidth, height: openPointY - closePointY) 512 | 513 | } else if(openPointY < closePointY) { 514 | fillCandleColor = theme.fallColor 515 | candleRect = CGRect(x: leftPosition, y: openPointY, width: theme.candleWidth, height: closePointY - openPointY) 516 | 517 | } else { 518 | candleRect = CGRect(x: leftPosition, y: closePointY, width: theme.candleWidth, height: theme.candleMinHeight) 519 | if(index > 0) { 520 | let preKLineModel = data[index - 1] 521 | if(model.open > preKLineModel.close) { 522 | fillCandleColor = theme.riseColor 523 | } else { 524 | fillCandleColor = theme.fallColor 525 | } 526 | } 527 | } 528 | 529 | // 主图 530 | if self.lineType == .minLineType { 531 | // 分时 532 | minLinePoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.close) * priceUnit + topChartPostionTop) 533 | minLine60MaPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.close) * priceUnit + topChartPostionTop) 534 | 535 | if self.mainDrawString == KLINEMA { 536 | // MA 537 | if model.ma60 > 0 { 538 | ma60Point = CGPoint(x: xPosition, y: (mainMaxPrice - model.ma60) * priceUnit + topChartPostionTop) 539 | } 540 | if ma60Point.y < topChartTop || ma60Point.y > midTextTop { 541 | ma60Point = .zero 542 | } 543 | }else if self.mainDrawString == KLINEBOLL { 544 | // BOLL 545 | midBollPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.boll_mb) * priceUnit + topChartPostionTop) 546 | } 547 | }else { 548 | // K线 549 | // 最高价 550 | if self.highestPrice < model.high { 551 | self.highestPrice = model.high 552 | self.highestPoint = highPoint 553 | } 554 | 555 | // 最低价 556 | if self.lowestPrice > model.low { 557 | self.lowestPrice = model.low 558 | self.lowestPoint = lowPoint 559 | } 560 | if self.mainDrawString == KLINEMA { 561 | // MA 562 | if model.ma5 > 0 { 563 | ma5Point = CGPoint(x: xPosition, y: (mainMaxPrice - model.ma5) * priceUnit + topChartPostionTop) 564 | } 565 | if ma5Point.y < topChartTop || ma5Point.y > midTextTop { 566 | ma5Point = .zero 567 | } 568 | 569 | if model.ma10 > 0 { 570 | ma10Point = CGPoint(x: xPosition, y: (mainMaxPrice - model.ma10) * priceUnit + topChartPostionTop) 571 | } 572 | 573 | if ma10Point.y < topChartTop || ma10Point.y > midTextTop { 574 | ma10Point = .zero 575 | } 576 | 577 | if model.ma30 > 0 { 578 | ma30Point = CGPoint(x: xPosition, y: (mainMaxPrice - model.ma30) * priceUnit + topChartPostionTop) 579 | } 580 | 581 | if ma30Point.y < topChartTop || ma30Point.y > midTextTop { 582 | ma30Point = .zero 583 | } 584 | 585 | }else if self.mainDrawString == KLINEBOLL { 586 | // BOLL 587 | upBollPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.boll_up) * priceUnit + topChartPostionTop) 588 | midBollPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.boll_mb) * priceUnit + topChartPostionTop) 589 | lowBollPoint = CGPoint(x: xPosition, y: (mainMaxPrice - model.boll_dn) * priceUnit + topChartPostionTop) 590 | } 591 | } 592 | 593 | // 副图 594 | if self.secondDrawString == KLINEVOL { 595 | // VOL 596 | var volume = (model.volumefrom - self.minVolume) * bottomChartUnit 597 | // 小于1 默认给1的高度 598 | if volume < theme.minVolHeight { 599 | volume = theme.minVolHeight 600 | } 601 | volumeStartPoint = CGPoint(x: xPosition, y: timeTextTop - volume) 602 | volumeEndPoint = CGPoint(x: xPosition, y: timeTextTop) 603 | 604 | let volMa5Y = (self.maxVolume - model.volMa5) * bottomChartUnit + bottomChartPostionTop 605 | if model.volMa5 > 0, volMa5Y > 0, volMa5Y <= timeTextTop { 606 | volMa5Point = CGPoint(x: xPosition, y: volMa5Y) 607 | } 608 | 609 | let volMa10Y = (self.maxVolume - model.volMa10) * bottomChartUnit + bottomChartPostionTop 610 | if model.volMa10 > 0, volMa10Y > 0, volMa10Y <= timeTextTop { 611 | volMa10Point = CGPoint(x: xPosition, y: volMa10Y) 612 | } 613 | }else if self.secondDrawString == KLINEMACD { 614 | // MACD 615 | let macd_bar = abs(model.macd_bar) * bottomChartUnit 616 | macdStartPoint = CGPoint(x: xPosition, y: startMacdBarY) 617 | if model.macd_bar > 0 { 618 | macdEndPoint = CGPoint(x: xPosition, y: startMacdBarY - macd_bar) 619 | macdBarColor = theme.riseColor 620 | }else if model.macd_bar < 0 { 621 | macdEndPoint = CGPoint(x: xPosition, y: startMacdBarY + macd_bar) 622 | macdBarColor = theme.fallColor 623 | }else { 624 | macdBarColor = UIColor.clear 625 | } 626 | 627 | let diffY = (self.maxMACD - model.macd_diff) * bottomChartUnit + bottomChartPostionTop 628 | diffPoint = CGPoint(x: xPosition, y: diffY) 629 | 630 | let deaY = (self.maxMACD - model.macd_dea) * bottomChartUnit + bottomChartPostionTop 631 | deaPoint = CGPoint(x: xPosition, y: deaY) 632 | 633 | }else if self.secondDrawString == KLINEKDJ { 634 | // KDJ 635 | let kdj_K_Y = (self.maxKDJ - model.kdj_k) * bottomChartUnit + bottomChartPostionTop 636 | kdj_K_Point = CGPoint(x: xPosition, y: kdj_K_Y) 637 | 638 | let kdj_D_Y = (self.maxKDJ - model.kdj_d) * bottomChartUnit + bottomChartPostionTop 639 | kdj_D_Point = CGPoint(x: xPosition, y: kdj_D_Y) 640 | 641 | let kdj_J_Y = (self.maxKDJ - model.kdj_j) * bottomChartUnit + bottomChartPostionTop 642 | kdj_J_Point = CGPoint(x: xPosition, y: kdj_J_Y) 643 | 644 | }else if self.secondDrawString == KLINERSI { 645 | // RSI 646 | let rsiY = (self.maxRSI - model.rsi) * bottomChartUnit + bottomChartPostionTop 647 | rsiPoint = CGPoint(x: xPosition, y: rsiY) 648 | } 649 | 650 | let positionModel = XLKLineCoordModel() 651 | positionModel.highPoint = highPoint 652 | positionModel.lowPoint = lowPoint 653 | positionModel.openPoint = CGPoint(x: xPosition, y: openPointY) 654 | positionModel.closePoint = CGPoint(x: xPosition, y: closePointY) 655 | positionModel.closeY = closePointY 656 | positionModel.ma5Point = ma5Point 657 | positionModel.ma10Point = ma10Point 658 | positionModel.ma30Point = ma30Point 659 | positionModel.ma60Point = ma60Point 660 | positionModel.upBollPoint = upBollPoint 661 | positionModel.midBollPoint = midBollPoint 662 | positionModel.lowBollPoint = lowBollPoint 663 | positionModel.minLinePoint = minLinePoint 664 | positionModel.minLine60MaPoint = minLine60MaPoint 665 | positionModel.volumeStartPoint = volumeStartPoint 666 | positionModel.volumeEndPoint = volumeEndPoint 667 | positionModel.volMa5Point = volMa5Point 668 | positionModel.volMa10Point = volMa10Point 669 | positionModel.candleFillColor = fillCandleColor 670 | positionModel.candleRect = candleRect 671 | positionModel.kdj_K_Point = kdj_K_Point 672 | positionModel.kdj_D_Point = kdj_D_Point 673 | positionModel.kdj_J_Point = kdj_J_Point 674 | positionModel.macdStartPoint = macdStartPoint 675 | positionModel.macdEndPoint = macdEndPoint 676 | positionModel.deaPoint = deaPoint 677 | positionModel.diffPoint = diffPoint 678 | positionModel.macdBarColor = macdBarColor 679 | positionModel.rsiPoint = rsiPoint 680 | if index % axisGap == 0 { 681 | positionModel.isDrawAxis = true 682 | } 683 | self.positionModels.append(positionModel) 684 | self.klineModels.append(model) 685 | } 686 | } 687 | } 688 | 689 | // MARK: - 主图 690 | /// 画分时图 691 | func drawMinLineChartLayer(array: [XLKLineCoordModel]) { 692 | if array.count < 1 { 693 | return 694 | } 695 | 696 | let minLinePath = UIBezierPath() 697 | minLinePath.move(to: array.first!.minLinePoint) 698 | for index in 1 ..< array.count { 699 | minLinePath.addLine(to: array[index].minLinePoint) 700 | } 701 | 702 | minLineChartLayer.frame = self.bounds 703 | minLineChartLayer.path = minLinePath.cgPath 704 | minLineChartLayer.lineWidth = theme.frameWidth 705 | minLineChartLayer.strokeColor = theme.minLineColor.cgColor 706 | minLineChartLayer.fillColor = UIColor.clear.cgColor 707 | 708 | // 填充颜色 709 | minLinePath.addLine(to: CGPoint(x: array.last!.minLinePoint.x, y: midTextTop)) 710 | minLinePath.addLine(to: CGPoint(x: array[0].minLinePoint.x, y: midTextTop)) 711 | minLineFillColorLayer.frame = self.bounds 712 | minLineFillColorLayer.path = minLinePath.cgPath 713 | minLineFillColorLayer.fillColor = theme.minLinefillColor.cgColor 714 | minLineFillColorLayer.strokeColor = UIColor.clear.cgColor 715 | minLineFillColorLayer.zPosition -= 1 // 将图层置于下一级,让底部的标记线显示出来 716 | 717 | self.layer.addSublayer(minLineChartLayer) 718 | self.layer.addSublayer(minLineFillColorLayer) 719 | } 720 | 721 | /// 画分时均线图 722 | func drawMinLineMALayer(array: [XLKLineCoordModel]) { 723 | 724 | if array.count < 1 { 725 | return 726 | } 727 | 728 | let ma60LinePath = UIBezierPath() 729 | for index in 1 ..< array.count { 730 | let preMa60Point = array[index - 1].ma60Point 731 | let ma60Point = array[index].ma60Point 732 | if ma60Point != .zero, preMa60Point != .zero { 733 | ma60LinePath.move(to: preMa60Point) 734 | ma60LinePath.addLine(to: ma60Point) 735 | } 736 | } 737 | 738 | ma60LineLayer.frame = self.bounds 739 | ma60LineLayer.path = ma60LinePath.cgPath 740 | ma60LineLayer.strokeColor = theme.topTextOneColor.cgColor 741 | ma60LineLayer.fillColor = UIColor.clear.cgColor 742 | 743 | self.layer.addSublayer(ma60LineLayer) 744 | } 745 | 746 | /// 画分时图的BOLL线图 747 | func drawMinLineBOLLLayer(array: [XLKLineCoordModel]) { 748 | 749 | if array.count < 1 { 750 | return 751 | } 752 | 753 | let midBollLinePath = UIBezierPath() 754 | for index in 1 ..< array.count { 755 | let preMidBollPoint = array[index - 1].midBollPoint 756 | let midBollPoint = array[index].midBollPoint 757 | midBollLinePath.move(to: preMidBollPoint) 758 | midBollLinePath.addLine(to: midBollPoint) 759 | } 760 | 761 | midBollLineLayer.frame = bounds 762 | midBollLineLayer.path = midBollLinePath.cgPath 763 | midBollLineLayer.strokeColor = theme.topTextOneColor.cgColor 764 | midBollLineLayer.fillColor = UIColor.clear.cgColor 765 | 766 | self.layer.addSublayer(midBollLineLayer) 767 | } 768 | 769 | /// 画蜡烛图 770 | func drawCandleChartLayer(array: [XLKLineCoordModel]) { 771 | candleChartLayer.frame = bounds 772 | for object in array.enumerated() { 773 | let candleLayer = getCandleLayer(model: object.element) 774 | candleChartLayer.addSublayer(candleLayer) 775 | } 776 | self.layer.addSublayer(candleChartLayer) 777 | } 778 | 779 | /// 画主均线图 780 | func drawMainMALayer(array: [XLKLineCoordModel]) { 781 | 782 | if array.count < 1 { 783 | return 784 | } 785 | 786 | let ma5LinePath = UIBezierPath() 787 | let ma10LinePath = UIBezierPath() 788 | let ma30LinePath = UIBezierPath() 789 | for index in 1 ..< array.count { 790 | let preMa5Point = array[index - 1].ma5Point 791 | let ma5Point = array[index].ma5Point 792 | if ma5Point != .zero, preMa5Point != .zero { 793 | ma5LinePath.move(to: preMa5Point) 794 | ma5LinePath.addLine(to: ma5Point) 795 | } 796 | 797 | let preMa10Point = array[index - 1].ma10Point 798 | let ma10Point = array[index].ma10Point 799 | if ma10Point != .zero, preMa10Point != .zero { 800 | ma10LinePath.move(to: preMa10Point) 801 | ma10LinePath.addLine(to: ma10Point) 802 | } 803 | 804 | let preMa30Point = array[index - 1].ma30Point 805 | let ma30Point = array[index].ma30Point 806 | if ma30Point != .zero, preMa30Point != .zero { 807 | ma30LinePath.move(to: preMa30Point) 808 | ma30LinePath.addLine(to: ma30Point) 809 | } 810 | } 811 | 812 | ma5LineLayer.frame = self.bounds 813 | ma5LineLayer.path = ma5LinePath.cgPath 814 | ma5LineLayer.strokeColor = theme.topTextOneColor.cgColor 815 | ma5LineLayer.fillColor = UIColor.clear.cgColor 816 | 817 | ma10LineLayer.frame = self.bounds 818 | ma10LineLayer.path = ma10LinePath.cgPath 819 | ma10LineLayer.strokeColor = theme.topTextTwoColor.cgColor 820 | ma10LineLayer.fillColor = UIColor.clear.cgColor 821 | 822 | ma30LineLayer.frame = self.bounds 823 | ma30LineLayer.path = ma30LinePath.cgPath 824 | ma30LineLayer.strokeColor = theme.topTextThreeColor.cgColor 825 | ma30LineLayer.fillColor = UIColor.clear.cgColor 826 | 827 | self.layer.addSublayer(ma5LineLayer) 828 | self.layer.addSublayer(ma10LineLayer) 829 | self.layer.addSublayer(ma30LineLayer) 830 | } 831 | 832 | /// 画BOLL线图 833 | func drawBOLLLayer(array: [XLKLineCoordModel]) { 834 | 835 | if array.count < 1 { 836 | return 837 | } 838 | 839 | let upBollLinePath = UIBezierPath() 840 | let midBollLinePath = UIBezierPath() 841 | let lowBollLinePath = UIBezierPath() 842 | for index in 1 ..< array.count { 843 | let preUpBollPoint = array[index - 1].upBollPoint 844 | let upBollPoint = array[index].upBollPoint 845 | upBollLinePath.move(to: preUpBollPoint) 846 | upBollLinePath.addLine(to: upBollPoint) 847 | 848 | let preMidBollPoint = array[index - 1].midBollPoint 849 | let midBollPoint = array[index].midBollPoint 850 | midBollLinePath.move(to: preMidBollPoint) 851 | midBollLinePath.addLine(to: midBollPoint) 852 | 853 | let preLowPoint = array[index - 1].lowBollPoint 854 | let lowPoint = array[index].lowBollPoint 855 | lowBollLinePath.move(to: preLowPoint) 856 | lowBollLinePath.addLine(to: lowPoint) 857 | } 858 | 859 | upBollLineLayer.frame = bounds 860 | upBollLineLayer.path = upBollLinePath.cgPath 861 | upBollLineLayer.strokeColor = theme.topTextOneColor.cgColor 862 | upBollLineLayer.fillColor = UIColor.clear.cgColor 863 | 864 | midBollLineLayer.frame = bounds 865 | midBollLineLayer.path = midBollLinePath.cgPath 866 | midBollLineLayer.strokeColor = theme.topTextTwoColor.cgColor 867 | midBollLineLayer.fillColor = UIColor.clear.cgColor 868 | 869 | lowBollLineLayer.frame = bounds 870 | lowBollLineLayer.path = lowBollLinePath.cgPath 871 | lowBollLineLayer.strokeColor = theme.topTextThreeColor.cgColor 872 | lowBollLineLayer.fillColor = UIColor.clear.cgColor 873 | 874 | self.layer.addSublayer(upBollLineLayer) 875 | self.layer.addSublayer(midBollLineLayer) 876 | self.layer.addSublayer(lowBollLineLayer) 877 | } 878 | 879 | /// 绘制最高最低 880 | func drawHighLowLayer() { 881 | highLowLayer.moveHighLowLayer(highPoint: highestPoint, highPrice: highestPrice, lowPoint: lowestPoint, lowPrice: lowestPrice, startX: startX) 882 | self.layer.insertSublayer(highLowLayer, above: candleChartLayer) 883 | } 884 | 885 | // MARK: - 成交量 886 | /// 画成交量图 887 | func drawVolumeLayer(array: [XLKLineCoordModel]) { 888 | volumeLayer.frame = bounds 889 | for object in array.enumerated() { 890 | let model = object.element 891 | let volLayer = drawLine(lineWidth: theme.candleWidth + theme.candleGap * 0.5, 892 | startPoint: model.volumeStartPoint, 893 | endPoint: model.volumeEndPoint, 894 | strokeColor: model.candleFillColor, 895 | fillColor: model.candleFillColor) 896 | volumeLayer.addSublayer(volLayer) 897 | } 898 | self.layer.addSublayer(volumeLayer) 899 | } 900 | 901 | 902 | /// 画VOL均线图 903 | func drawVolMALayer(array: [XLKLineCoordModel]) { 904 | 905 | if array.count < 1 { 906 | return 907 | } 908 | 909 | let volMa5LinePath = UIBezierPath() 910 | let volMa10LinePath = UIBezierPath() 911 | 912 | for index in 1 ..< array.count { 913 | let preVolMa5Point = array[index - 1].volMa5Point 914 | let volMa5Point = array[index].volMa5Point 915 | if volMa5Point != .zero, preVolMa5Point != .zero { 916 | volMa5LinePath.move(to: preVolMa5Point) 917 | volMa5LinePath.addLine(to: volMa5Point) 918 | } 919 | 920 | let preVolMa10Point = array[index - 1].volMa10Point 921 | let volMa10Point = array[index].volMa10Point 922 | if volMa10Point != .zero, preVolMa10Point != .zero { 923 | volMa10LinePath.move(to: preVolMa10Point) 924 | volMa10LinePath.addLine(to: volMa10Point) 925 | } 926 | } 927 | 928 | volMa5LineLayer.frame = bounds 929 | volMa5LineLayer.path = volMa5LinePath.cgPath 930 | volMa5LineLayer.strokeColor = theme.bottomTextTwoColor.cgColor 931 | volMa5LineLayer.fillColor = UIColor.clear.cgColor 932 | 933 | volMa10LineLayer.frame = bounds 934 | volMa10LineLayer.path = volMa10LinePath.cgPath 935 | volMa10LineLayer.strokeColor = theme.bottomTextThreeColor.cgColor 936 | volMa10LineLayer.fillColor = UIColor.clear.cgColor 937 | 938 | self.layer.addSublayer(volMa5LineLayer) 939 | self.layer.addSublayer(volMa10LineLayer) 940 | } 941 | 942 | // MARK: - KDJ 943 | /// 画KDJ线图 944 | func drawKDJLayer(array: [XLKLineCoordModel]) { 945 | 946 | if array.count < 1 { 947 | return 948 | } 949 | 950 | let KDJ_K_LinePath = UIBezierPath() 951 | let KDJ_D_LinePath = UIBezierPath() 952 | let KDJ_J_LinePath = UIBezierPath() 953 | 954 | for index in 1 ..< array.count { 955 | let preKPoint = array[index - 1].kdj_K_Point 956 | let kPoint = array[index].kdj_K_Point 957 | if kPoint != .zero, preKPoint != .zero { 958 | KDJ_K_LinePath.move(to: preKPoint) 959 | KDJ_K_LinePath.addLine(to: kPoint) 960 | } 961 | 962 | let preDPoint = array[index - 1].kdj_D_Point 963 | let DPoint = array[index].kdj_D_Point 964 | if DPoint != .zero, preDPoint != .zero { 965 | KDJ_D_LinePath.move(to: preDPoint) 966 | KDJ_D_LinePath.addLine(to: DPoint) 967 | } 968 | 969 | let preJPoint = array[index - 1].kdj_J_Point 970 | let JPoint = array[index].kdj_J_Point 971 | if JPoint != .zero, preJPoint != .zero { 972 | KDJ_J_LinePath.move(to: preJPoint) 973 | KDJ_J_LinePath.addLine(to: JPoint) 974 | } 975 | } 976 | 977 | kdj_K_LineLayer.frame = bounds 978 | kdj_K_LineLayer.path = KDJ_K_LinePath.cgPath 979 | kdj_K_LineLayer.strokeColor = theme.bottomTextTwoColor.cgColor 980 | kdj_K_LineLayer.fillColor = UIColor.clear.cgColor 981 | 982 | kdj_D_LineLayer.frame = bounds 983 | kdj_D_LineLayer.path = KDJ_D_LinePath.cgPath 984 | kdj_D_LineLayer.strokeColor = theme.bottomTextThreeColor.cgColor 985 | kdj_D_LineLayer.fillColor = UIColor.clear.cgColor 986 | 987 | kdj_J_LineLayer.frame = bounds 988 | kdj_J_LineLayer.path = KDJ_J_LinePath.cgPath 989 | kdj_J_LineLayer.strokeColor = theme.bottomTextFourColor.cgColor 990 | kdj_J_LineLayer.fillColor = UIColor.clear.cgColor 991 | 992 | self.layer.addSublayer(kdj_K_LineLayer) 993 | self.layer.addSublayer(kdj_D_LineLayer) 994 | self.layer.addSublayer(kdj_J_LineLayer) 995 | } 996 | 997 | // MARK: - MACD 998 | /// 画MACD柱状图 999 | func drawMACD_BAR_Layer(array: [XLKLineCoordModel]) { 1000 | macd_barLayer.frame = bounds 1001 | for object in array.enumerated() { 1002 | let model = object.element 1003 | let volLayer = drawLine(lineWidth: theme.macdBarWidth, 1004 | startPoint: model.macdStartPoint, 1005 | endPoint: model.macdEndPoint, 1006 | strokeColor: model.macdBarColor, 1007 | fillColor: model.macdBarColor) 1008 | macd_barLayer.addSublayer(volLayer) 1009 | } 1010 | self.layer.addSublayer(macd_barLayer) 1011 | } 1012 | 1013 | /// 画MACD线图 1014 | func drawMACD_DIF_DEA_Layer(array: [XLKLineCoordModel]) { 1015 | 1016 | if array.count < 1 { 1017 | return 1018 | } 1019 | 1020 | let diffLinePath = UIBezierPath() 1021 | let deaLinePath = UIBezierPath() 1022 | 1023 | for index in 1 ..< array.count { 1024 | let preDiffPoint = array[index - 1].diffPoint 1025 | let diffPoint = array[index].diffPoint 1026 | if diffPoint != .zero, preDiffPoint != .zero { 1027 | diffLinePath.move(to: preDiffPoint) 1028 | diffLinePath.addLine(to: diffPoint) 1029 | } 1030 | 1031 | let preDeaPoint = array[index - 1].deaPoint 1032 | let DeaPoint = array[index].deaPoint 1033 | if DeaPoint != .zero, preDeaPoint != .zero { 1034 | deaLinePath.move(to: preDeaPoint) 1035 | deaLinePath.addLine(to: DeaPoint) 1036 | } 1037 | } 1038 | 1039 | macd_diffLayer.frame = bounds 1040 | macd_diffLayer.path = diffLinePath.cgPath 1041 | macd_diffLayer.strokeColor = theme.bottomTextThreeColor.cgColor 1042 | macd_diffLayer.fillColor = UIColor.clear.cgColor 1043 | 1044 | macd_deaLayer.frame = bounds 1045 | macd_deaLayer.path = deaLinePath.cgPath 1046 | macd_deaLayer.strokeColor = theme.bottomTextFourColor.cgColor 1047 | macd_deaLayer.fillColor = UIColor.clear.cgColor 1048 | 1049 | self.layer.addSublayer(macd_diffLayer) 1050 | self.layer.addSublayer(macd_deaLayer) 1051 | } 1052 | 1053 | // MARK: - RSI 1054 | func drawRsiLayer(array: [XLKLineCoordModel]) { 1055 | 1056 | if array.count < 1 { 1057 | return 1058 | } 1059 | 1060 | let rsiLinePath = UIBezierPath() 1061 | 1062 | for index in 1 ..< array.count { 1063 | let preRsiPoint = array[index - 1].rsiPoint 1064 | let rsiPoint = array[index].rsiPoint 1065 | if rsiPoint != .zero, preRsiPoint != .zero { 1066 | rsiLinePath.move(to: preRsiPoint) 1067 | rsiLinePath.addLine(to: rsiPoint) 1068 | } 1069 | } 1070 | 1071 | rsiLineLayer.frame = bounds 1072 | rsiLineLayer.path = rsiLinePath.cgPath 1073 | rsiLineLayer.strokeColor = theme.bottomTextTwoColor.cgColor 1074 | rsiLineLayer.fillColor = UIColor.clear.cgColor 1075 | 1076 | self.layer.addSublayer(rsiLineLayer) 1077 | } 1078 | 1079 | // MARK: - 时间layer 1080 | func drawxAxisTimeMarkLayer() { 1081 | xAxisTimeMarkLayer.sublayers?.removeAll() 1082 | for (index, position) in positionModels.enumerated() { 1083 | let model = klineModels[index] 1084 | if position.isDrawAxis { 1085 | if dateType == .min { 1086 | xAxisTimeMarkLayer.addSublayer(drawXaxisTimeMark(xPosition: position.highPoint.x, dateString: model.time.xlChart.toTimeString("MM/dd HH:mm"))) 1087 | }else if dateType == .day { 1088 | xAxisTimeMarkLayer.addSublayer(drawXaxisTimeMark(xPosition: position.highPoint.x, dateString: model.time.xlChart.toTimeString("YY/MM/dd"))) 1089 | } 1090 | } 1091 | } 1092 | self.layer.addSublayer(xAxisTimeMarkLayer) 1093 | } 1094 | 1095 | // MARK: - 清除图层 1096 | func clearLayer() { 1097 | 1098 | // 时间 1099 | xAxisTimeMarkLayer.sublayers?.forEach{ $0.removeFromSuperlayer() } 1100 | 1101 | // 主图 1102 | minLineChartLayer.removeFromSuperlayer() 1103 | minLineFillColorLayer.removeFromSuperlayer() 1104 | ma60LineLayer.removeFromSuperlayer() 1105 | candleChartLayer.sublayers?.forEach{ $0.removeFromSuperlayer() } 1106 | clearMainMaLayer() 1107 | clearMainBollLayer() 1108 | 1109 | // 副图 1110 | // VOL 1111 | volumeLayer.sublayers?.forEach{ $0.removeFromSuperlayer() } 1112 | volMa5LineLayer.removeFromSuperlayer() 1113 | volMa10LineLayer.removeFromSuperlayer() 1114 | 1115 | // KDJ 1116 | kdj_K_LineLayer.removeFromSuperlayer() 1117 | kdj_D_LineLayer.removeFromSuperlayer() 1118 | kdj_J_LineLayer.removeFromSuperlayer() 1119 | 1120 | // MACD 1121 | macd_barLayer.sublayers?.forEach{ $0.removeFromSuperlayer() } 1122 | macd_deaLayer.removeFromSuperlayer() 1123 | macd_diffLayer.removeFromSuperlayer() 1124 | 1125 | // RSI 1126 | rsiLineLayer.removeFromSuperlayer() 1127 | } 1128 | 1129 | func clearMainMaLayer() { 1130 | ma5LineLayer.removeFromSuperlayer() 1131 | ma10LineLayer.removeFromSuperlayer() 1132 | ma30LineLayer.removeFromSuperlayer() 1133 | } 1134 | 1135 | func clearMainBollLayer() { 1136 | midBollLineLayer.removeFromSuperlayer() 1137 | upBollLineLayer.removeFromSuperlayer() 1138 | lowBollLineLayer.removeFromSuperlayer() 1139 | } 1140 | 1141 | // MARK: - 私有 1142 | /// 获取单个蜡烛图的layer 1143 | fileprivate func getCandleLayer(model: XLKLineCoordModel) -> XLCAShapeLayer { 1144 | // K线 1145 | let linePath = UIBezierPath(rect: model.candleRect) 1146 | 1147 | // 影线 1148 | linePath.move(to: model.lowPoint) 1149 | linePath.addLine(to: model.highPoint) 1150 | 1151 | let klayer = XLCAShapeLayer() 1152 | klayer.path = linePath.cgPath 1153 | klayer.strokeColor = model.candleFillColor.cgColor 1154 | klayer.fillColor = model.candleFillColor.cgColor 1155 | 1156 | return klayer 1157 | } 1158 | 1159 | /// 横坐标单个时间标签 1160 | fileprivate func drawXaxisTimeMark(xPosition: CGFloat, dateString: String) -> XLCAShapeLayer { 1161 | 1162 | let linePath = UIBezierPath() 1163 | linePath.move(to: CGPoint(x: xPosition, y: theme.topTextHeight)) 1164 | linePath.addLine(to: CGPoint(x: xPosition, y: midTextTop)) 1165 | 1166 | linePath.move(to: CGPoint(x: xPosition, y: bottomChartTop)) 1167 | linePath.addLine(to: CGPoint(x: xPosition, y: timeTextTop)) 1168 | 1169 | let lineLayer = XLCAShapeLayer() 1170 | lineLayer.path = linePath.cgPath 1171 | lineLayer.lineWidth = theme.frameWidth 1172 | lineLayer.strokeColor = theme.lineBorderColor.cgColor 1173 | // lineLayer.fillColor = UIColor.orange.cgColor 1174 | // lineLayer.backgroundColor = theme.lineBorderColor.cgColor 1175 | 1176 | let textSize = theme.getTextSize(text: dateString) 1177 | 1178 | var labelX: CGFloat = 0 1179 | var labelY: CGFloat = 0 1180 | let maxX = frame.maxX - textSize.width 1181 | labelX = xPosition - textSize.width / 2.0 1182 | labelY = timeTextTop + 6 1183 | if labelX > maxX { 1184 | labelX = maxX 1185 | } else if labelX < frame.minX { 1186 | labelX = frame.minX 1187 | } 1188 | 1189 | let timeLayer = drawTextLayer(frame: CGRect(x: labelX, y: labelY, width: textSize.width, height: textSize.height), text: dateString, foregroundColor: theme.textColor) 1190 | 1191 | let shaperLayer = XLCAShapeLayer() 1192 | shaperLayer.addSublayer(lineLayer) 1193 | shaperLayer.addSublayer(timeLayer) 1194 | 1195 | return shaperLayer 1196 | } 1197 | 1198 | } 1199 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLKLineCoordModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLKLineCoordModel.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// K线坐标Model 12 | public class XLKLineCoordModel: NSObject { 13 | 14 | var openPoint: CGPoint = .zero 15 | var closePoint: CGPoint = .zero 16 | var highPoint: CGPoint = .zero 17 | var lowPoint: CGPoint = .zero 18 | 19 | var ma5Point: CGPoint = .zero 20 | var ma10Point: CGPoint = .zero 21 | var ma30Point: CGPoint = .zero 22 | 23 | var ma60Point: CGPoint = .zero 24 | 25 | var midBollPoint: CGPoint = .zero 26 | var upBollPoint: CGPoint = .zero 27 | var lowBollPoint: CGPoint = .zero 28 | 29 | var minLinePoint: CGPoint = .zero 30 | var minLine60MaPoint: CGPoint = .zero 31 | 32 | var volumeStartPoint: CGPoint = .zero 33 | var volumeEndPoint: CGPoint = .zero 34 | 35 | var volMa5Point: CGPoint = .zero 36 | var volMa10Point: CGPoint = .zero 37 | 38 | var kdj_K_Point: CGPoint = .zero 39 | var kdj_D_Point: CGPoint = .zero 40 | var kdj_J_Point: CGPoint = .zero 41 | 42 | var macdStartPoint: CGPoint = .zero 43 | var macdEndPoint: CGPoint = .zero 44 | var diffPoint: CGPoint = .zero 45 | var deaPoint: CGPoint = .zero 46 | var macdBarColor: UIColor = UIColor.black 47 | 48 | var rsiPoint: CGPoint = .zero 49 | 50 | var candleFillColor: UIColor = UIColor.black 51 | var candleRect: CGRect = CGRect.zero 52 | 53 | var closeY: CGFloat = 0 54 | 55 | var isDrawAxis: Bool = false 56 | } 57 | 58 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLKLineModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLKLineModel.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// K线数据Model 12 | public class XLKLineModel: NSObject { 13 | 14 | /// 开盘 15 | public var open: CGFloat = 0 16 | 17 | /// 收盘 18 | public var close: CGFloat = 0 19 | 20 | /// 最高 21 | public var high: CGFloat = 0 22 | 23 | /// 最低 24 | public var low: CGFloat = 0 25 | 26 | /// 成交量 27 | public var volumefrom: CGFloat = 0 28 | 29 | /// 成交额 30 | public var volumeto: CGFloat = 0 31 | 32 | /// 时间 33 | public var time: TimeInterval = 0 34 | 35 | /// 流入 36 | public var inflow: CGFloat = 0 37 | 38 | /// 流出 39 | public var outflow: CGFloat = 0 40 | 41 | /// boll中线 42 | public var boll_mb: CGFloat = 0 43 | 44 | /// boll上线 45 | public var boll_up: CGFloat = 0 46 | 47 | /// boll下线 48 | public var boll_dn: CGFloat = 0 49 | 50 | /// ma5值 51 | public var ma5: CGFloat = 0 52 | 53 | /// ma10值 54 | public var ma10: CGFloat = 0 55 | 56 | /// ma30值 57 | public var ma30: CGFloat = 0 58 | 59 | /// ma60值 60 | public var ma60: CGFloat = 0 61 | 62 | /// volMa5值 63 | public var volMa5: CGFloat = 0 64 | 65 | /// volMa10值 66 | public var volMa10: CGFloat = 0 67 | 68 | /// macd diff线 69 | public var macd_diff: CGFloat = 0 70 | 71 | /// macd dea线 72 | public var macd_dea: CGFloat = 0 73 | 74 | /// macd 柱状图 75 | public var macd_bar: CGFloat = 0 76 | 77 | /// kdj k线 78 | public var kdj_k: CGFloat = 0 79 | 80 | /// kdj d线 81 | public var kdj_d: CGFloat = 0 82 | 83 | /// kdj j线 84 | public var kdj_j: CGFloat = 0 85 | 86 | /// rsi 87 | public var rsi: CGFloat = 0 88 | } 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLKLineScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLKLineScrollView.swift 3 | // behoo 4 | // 5 | // Created by 夏磊 on 2018/8/23. 6 | // Copyright © 2018年 behoo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// K线ScrollView 用于手势冲突类区分 12 | open class XLKLineScrollView: UIScrollView { 13 | 14 | override init(frame: CGRect) { 15 | super.init(frame: frame) 16 | 17 | if #available(iOS 11.0, *) { 18 | contentInsetAdjustmentBehavior = .never 19 | } 20 | } 21 | 22 | required public init?(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLKLineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLKLineView.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/22. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol XLKLineViewProtocol: class { 12 | func XLKLineViewLoadMore() 13 | func XLKLineViewLongPress(model: XLKLineModel, preClose: CGFloat) 14 | func XLKLineViewHideCrossDetail() 15 | } 16 | 17 | open class XLKLineView: UIView, XLDrawLayerProtocol { 18 | 19 | @objc public weak var kLineViewDelegate: XLKLineViewProtocol? = nil 20 | public var theme = XLKLineStyle() 21 | public var allDataK: [XLKLineModel] = [] 22 | 23 | var dataK: [XLKLineModel] = [] 24 | var enableKVO: Bool = true 25 | var isLongPressEnd = false 26 | 27 | var kLineViewWidth: CGFloat = 0.0 28 | var preKLineRightOffset: CGFloat = 0 29 | 30 | var mainDrawString: String = KLINEMA 31 | var secondDrawString: String = KLINEVOL 32 | var dateType: XLKLineDateType = .min 33 | var lineType: XLKLineType = .candleLineType 34 | 35 | var scrollView: XLKLineScrollView! 36 | var kLine: XLKLine! 37 | 38 | var frameLayer: XLCAShapeLayer! 39 | var topChartTextLayer: XLTopChartTextLayer! 40 | var bottomChartTextLayer: XLBottomChartTextLayer! 41 | var midChartTextLayer: XLMidChartTextLayer! 42 | var crossLineLayer: XLCrossLineLayer! 43 | 44 | /// 上一次渲染的offsetX 45 | var preRenderOffsetX:CGFloat = 0 46 | 47 | // 上一次手指一动的x 48 | var preOneTouchX: CGFloat = 0 49 | var preTwoTouchX: CGFloat = 0 50 | 51 | // 上一次长按的index 用于震动反馈 52 | var preHighLightIndex: Int = 0 53 | 54 | @available(iOS 10.0, *) 55 | lazy var generator: UIImpactFeedbackGenerator = { 56 | let generator = UIImpactFeedbackGenerator.init(style: .light) 57 | return generator 58 | }() 59 | 60 | 61 | var topChartHeight: CGFloat { 62 | get { 63 | return frame.height - theme.topTextHeight - theme.midTextHeight - theme.bottomChartHeight - theme.timeTextHeight 64 | } 65 | } 66 | 67 | // 横向分隔线间距 68 | var topChartLineMargin: CGFloat { 69 | get { 70 | return self.topChartHeight / 4 71 | } 72 | } 73 | 74 | var topChartTop: CGFloat { 75 | get { 76 | return theme.topTextHeight 77 | } 78 | } 79 | 80 | var midTextTop: CGFloat { 81 | get { 82 | return theme.topTextHeight + topChartHeight 83 | } 84 | } 85 | 86 | var bottomChartTop: CGFloat { 87 | get { 88 | return theme.topTextHeight + topChartHeight + theme.midTextHeight 89 | } 90 | } 91 | 92 | var timeTextTop: CGFloat { 93 | get { 94 | return theme.topTextHeight + topChartHeight + theme.midTextHeight + theme.bottomChartHeight 95 | } 96 | } 97 | 98 | public override init(frame: CGRect) { 99 | super.init(frame: frame) 100 | 101 | drawFrameLayer() 102 | 103 | scrollView = XLKLineScrollView(frame: bounds) 104 | scrollView.backgroundColor = UIColor.clear 105 | scrollView.showsHorizontalScrollIndicator = false 106 | scrollView.showsVerticalScrollIndicator = false 107 | scrollView.bounces = false 108 | 109 | scrollView.delegate = self 110 | scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: .new, context: nil) 111 | addSubview(scrollView) 112 | 113 | kLine = XLKLine(frame: bounds) 114 | scrollView.addSubview(kLine) 115 | 116 | topChartTextLayer = XLTopChartTextLayer() 117 | topChartTextLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: theme.topTextHeight) 118 | layer.addSublayer(topChartTextLayer) 119 | 120 | bottomChartTextLayer = XLBottomChartTextLayer() 121 | bottomChartTextLayer.frame = CGRect(x: 0, y: midTextTop, width: frame.width, height: theme.midTextHeight) 122 | layer.addSublayer(bottomChartTextLayer) 123 | 124 | midChartTextLayer = XLMidChartTextLayer() 125 | midChartTextLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height - theme.timeTextHeight - theme.bottomChartHeight) 126 | layer.addSublayer(midChartTextLayer) 127 | 128 | crossLineLayer = XLCrossLineLayer() 129 | crossLineLayer.frame = bounds 130 | layer.addSublayer(crossLineLayer) 131 | 132 | let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGestureAction(_:))) 133 | kLine.addGestureRecognizer(longPressGesture) 134 | 135 | let pinGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinGestureAction(_:))) 136 | kLine.addGestureRecognizer(pinGesture) 137 | 138 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureAction(_:))) 139 | kLine.addGestureRecognizer(tapGesture) 140 | } 141 | 142 | required public init?(coder aDecoder: NSCoder) { 143 | fatalError("init(coder:) has not been implemented") 144 | } 145 | 146 | deinit { 147 | if let scrollView = scrollView { 148 | scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset)) 149 | } 150 | } 151 | 152 | override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 153 | if keyPath == #keyPath(UIScrollView.contentOffset) && enableKVO { 154 | // print("in klineview scrollView?.contentOffset.x " + "\(scrollView.contentOffset.x)") 155 | 156 | // 当滚动间隙大于1个K线柱的时候 才刷新视图 157 | if abs(preRenderOffsetX - scrollView.contentOffset.x) < (theme.candleWidth+theme.candleGap), scrollView.contentSize.width > scrollView.bounds.size.width { 158 | return 159 | } 160 | // 拖动 ScrollView 时重绘当前显示的 klineview 161 | kLine.contentOffsetX = scrollView.contentOffset.x 162 | kLine.renderWidth = scrollView.frame.width 163 | kLine.drawKLineView(mainDrawString: mainDrawString, secondDrawString: secondDrawString, lineType: lineType) 164 | frameLayer.isHidden = false 165 | 166 | var sixNum: CGFloat = 0 167 | if secondDrawString == KLINEVOL { 168 | sixNum = kLine.maxVolume 169 | }else if secondDrawString == KLINEMACD { 170 | sixNum = kLine.maxMACD 171 | }else if secondDrawString == KLINEKDJ { 172 | sixNum = kLine.maxKDJ 173 | }else if secondDrawString == KLINERSI { 174 | sixNum = kLine.maxRSI 175 | } 176 | 177 | midChartTextLayer.configurePriceVolue(one: kLine.onePrice, two: kLine.twoPrice, three: kLine.threePrice, four: kLine.fourPrice, five: kLine.fivePrice, six: sixNum) 178 | preRenderOffsetX = scrollView.contentOffset.x 179 | } 180 | } 181 | 182 | /// 首次加载需要计算MA VOL的值 183 | func configureMidText() { 184 | 185 | // 主图 186 | var one: CGFloat = 0 187 | var two: CGFloat = 0 188 | var three: CGFloat = 0 189 | 190 | if self.mainDrawString == KLINEMA { 191 | if lineType == .minLineType { 192 | // 分时 用MA60 193 | one = dataK.last?.ma60 ?? 0 194 | }else { 195 | // K线 用MA5的数据 196 | one = dataK.last?.ma5 ?? 0 197 | } 198 | two = dataK.last?.ma10 ?? 0 199 | three = dataK.last?.ma30 ?? 0 200 | }else if self.mainDrawString == KLINEBOLL { 201 | one = dataK.last?.boll_up ?? 0 202 | two = dataK.last?.boll_mb ?? 0 203 | three = dataK.last?.boll_dn ?? 0 204 | } 205 | topChartTextLayer.configureTopValue(lineType: lineType, mainDrawString: mainDrawString, one: one, two: two, three: three) 206 | 207 | // 副图 208 | if self.secondDrawString == KLINEVOL { 209 | one = dataK.last?.volumefrom ?? 0 210 | two = calcVolMa(num: 5, targetIndex: dataK.count - 1, isMustHasNum: true, dataK: dataK) ?? 0 211 | three = calcVolMa(num: 10, targetIndex: dataK.count - 1, isMustHasNum: true, dataK: dataK) ?? 0 212 | }else if self.secondDrawString == KLINEMACD { 213 | one = dataK.last?.macd_bar ?? 0 214 | two = dataK.last?.macd_diff ?? 0 215 | three = dataK.last?.macd_dea ?? 0 216 | }else if self.secondDrawString == KLINEKDJ { 217 | one = dataK.last?.kdj_k ?? 0 218 | two = dataK.last?.kdj_d ?? 0 219 | three = dataK.last?.kdj_j ?? 0 220 | }else if self.secondDrawString == KLINERSI { 221 | one = dataK.last?.rsi ?? 0 222 | } 223 | bottomChartTextLayer.configureBottomValue(secondDrawString: secondDrawString, one: one, two: two, three: three) 224 | } 225 | 226 | /// 加载k线数据的方法 mainDrawString:主图绘制字符串,secondDrawString:副图绘制字符串,dateType:日期显示类型,lineType:K线显示类型 227 | public func configureView(data: [XLKLineModel], isNew: Bool, mainDrawString: String, secondDrawString: String, dateType: XLKLineDateType, lineType: XLKLineType) { 228 | 229 | hideCross() 230 | self.mainDrawString = mainDrawString 231 | self.secondDrawString = secondDrawString 232 | self.dateType = dateType 233 | self.lineType = lineType 234 | preRenderOffsetX = 0 235 | 236 | if isNew { 237 | self.dataK = data 238 | }else { 239 | self.dataK = data + self.dataK 240 | } 241 | self.kLine.dataK = self.dataK 242 | self.kLine.dateType = dateType 243 | self.kLine.lineType = lineType 244 | 245 | let count: CGFloat = CGFloat(dataK.count) 246 | 247 | // 总长度 248 | kLineViewWidth = count * theme.candleWidth + (count + 1) * theme.candleGap 249 | if kLineViewWidth < self.frame.width { 250 | kLineViewWidth = self.frame.width 251 | } 252 | 253 | // 更新view长度 254 | // print("currentWidth " + "\(kLineViewWidth)") 255 | kLine.frame = CGRect(x: 0, y: 0, width: kLineViewWidth, height: scrollView.frame.height) 256 | 257 | var contentOffsetX: CGFloat = 0 258 | 259 | if isNew { 260 | // 首次加载,将 kLine 的右边和scrollview的右边对齐 261 | contentOffsetX = kLine.frame.width - scrollView.frame.width 262 | 263 | // 首次加载需要计算MA VOL的值 264 | configureMidText() 265 | 266 | }else { 267 | if scrollView.contentSize.width > 0 { 268 | contentOffsetX = kLineViewWidth - preKLineRightOffset 269 | } else { 270 | // 首次加载,将 kLine 的右边和scrollview的右边对齐 271 | contentOffsetX = kLine.frame.width - scrollView.frame.width 272 | 273 | // 首次加载需要计算MA VOL的值 274 | configureMidText() 275 | } 276 | } 277 | 278 | scrollView.contentSize = CGSize(width: kLineViewWidth, height: self.frame.height) 279 | scrollView.contentOffset = CGPoint(x: contentOffsetX, y: 0) 280 | kLine.contentOffsetX = scrollView.contentOffset.x 281 | // print("ScrollKLine contentOffsetX " + "\(contentOffsetX)") 282 | } 283 | 284 | func updateKlineViewWidth() { 285 | let count: CGFloat = CGFloat(kLine.dataK.count) 286 | // 总长度 287 | kLineViewWidth = count * theme.candleWidth + (count + 1) * theme.candleGap 288 | if kLineViewWidth < self.frame.width { 289 | kLineViewWidth = self.frame.width 290 | } 291 | 292 | // 更新view长度 293 | // print("currentWidth " + "\(kLineViewWidth)") 294 | kLine.frame = CGRect(x: 0, y: 0, width: kLineViewWidth, height: scrollView.frame.height) 295 | scrollView.contentSize = CGSize(width: kLineViewWidth, height: self.frame.height) 296 | } 297 | 298 | // 画边框 299 | func drawFrameLayer() { 300 | 301 | let linePath = UIBezierPath() 302 | 303 | // 横向分隔线 304 | linePath.move(to: CGPoint(x: -50, y: 0)) 305 | linePath.addLine(to: CGPoint(x: frame.maxX + 50, y: 0)) 306 | 307 | linePath.move(to: CGPoint(x: 0, y: theme.topTextHeight)) 308 | linePath.addLine(to: CGPoint(x: frame.width, y: theme.topTextHeight)) 309 | 310 | linePath.move(to: CGPoint(x: 0, y: midTextTop)) 311 | linePath.addLine(to: CGPoint(x: frame.width, y: midTextTop)) 312 | 313 | linePath.move(to: CGPoint(x: 0, y: bottomChartTop)) 314 | linePath.addLine(to: CGPoint(x: frame.width, y: bottomChartTop)) 315 | 316 | linePath.move(to: CGPoint(x: 0, y: bottomChartTop + theme.bottomChartHeight * 0.5)) 317 | linePath.addLine(to: CGPoint(x: frame.width, y: bottomChartTop + theme.bottomChartHeight * 0.5)) 318 | 319 | linePath.move(to: CGPoint(x: -50, y: timeTextTop)) 320 | linePath.addLine(to: CGPoint(x: frame.maxX + 50, y: timeTextTop)) 321 | 322 | linePath.move(to: CGPoint(x: -50, y: frame.height)) 323 | linePath.addLine(to: CGPoint(x: frame.maxX + 50, y: frame.height)) 324 | 325 | // topChart分隔线 326 | linePath.move(to: CGPoint(x: 0, y: topChartLineMargin + topChartTop)) 327 | linePath.addLine(to: CGPoint(x: frame.width, y: topChartLineMargin + topChartTop)) 328 | 329 | linePath.move(to: CGPoint(x: 0, y: topChartLineMargin * 2 + topChartTop)) 330 | linePath.addLine(to: CGPoint(x: frame.width, y: topChartLineMargin * 2 + topChartTop)) 331 | 332 | linePath.move(to: CGPoint(x: 0, y: topChartLineMargin * 3 + topChartTop)) 333 | linePath.addLine(to: CGPoint(x: frame.width, y: topChartLineMargin * 3 + topChartTop)) 334 | 335 | frameLayer = XLCAShapeLayer() 336 | frameLayer.path = linePath.cgPath 337 | frameLayer.lineWidth = theme.frameWidth 338 | frameLayer.strokeColor = theme.lineBorderColor.cgColor 339 | frameLayer.isHidden = true 340 | 341 | self.layer.addSublayer(frameLayer) 342 | } 343 | 344 | func beginShowCross(_ recognizer: UIGestureRecognizer) { 345 | let point = recognizer.location(in: kLine) 346 | var highLightIndex = Int(point.x / (theme.candleWidth + theme.candleGap)) 347 | var positionModelIndex = highLightIndex - kLine.startIndex 348 | highLightIndex = highLightIndex <= 0 ? 0 : highLightIndex 349 | positionModelIndex = positionModelIndex <= 0 ? 0 : positionModelIndex 350 | 351 | if highLightIndex < kLine.dataK.count && positionModelIndex < kLine.positionModels.count { 352 | 353 | // 震动反馈 354 | if preHighLightIndex != highLightIndex { 355 | if #available(iOS 10.0, *) { 356 | generator.prepare() 357 | generator.impactOccurred() 358 | } 359 | } 360 | preHighLightIndex = highLightIndex 361 | 362 | let entity = kLine.dataK[highLightIndex] 363 | let left = kLine.startX + CGFloat(highLightIndex - kLine.startIndex) * (self.theme.candleWidth + theme.candleGap) - scrollView.contentOffset.x 364 | let centerX = left + theme.candleWidth / 2.0 365 | let highLightVolume = kLine.positionModels[positionModelIndex].volumeStartPoint.y 366 | let highLightClose = kLine.positionModels[positionModelIndex].closeY 367 | let preIndex = (highLightIndex - 1 >= 0) ? (highLightIndex - 1) : highLightIndex 368 | let preData = kLine.dataK[preIndex] 369 | 370 | // 顶部数据 371 | var one: CGFloat = 0 372 | var two: CGFloat = 0 373 | var three: CGFloat = 0 374 | 375 | if self.mainDrawString == KLINEMA { 376 | if lineType == .minLineType { 377 | // 分时 用MA60 378 | one = entity.ma60 379 | }else { 380 | // K线 用MA5的数据 381 | one = entity.ma5 382 | } 383 | two = entity.ma10 384 | three = entity.ma30 385 | }else if self.mainDrawString == KLINEBOLL { 386 | one = entity.boll_up 387 | two = entity.boll_mb 388 | three = entity.boll_dn 389 | } 390 | topChartTextLayer.configureTopValue(lineType: lineType, mainDrawString: mainDrawString, one: one, two: two, three: three) 391 | 392 | // 底部数据 393 | if self.secondDrawString == KLINEVOL { 394 | one = entity.volumefrom 395 | two = calcVolMa(num: 5, targetIndex: highLightIndex, isMustHasNum: true, dataK: dataK) ?? 0 396 | three = calcVolMa(num: 10, targetIndex: highLightIndex, isMustHasNum: true, dataK: dataK) ?? 0 397 | }else if self.secondDrawString == KLINEMACD { 398 | one = entity.macd_bar 399 | two = entity.macd_diff 400 | three = entity.macd_dea 401 | }else if self.secondDrawString == KLINEKDJ { 402 | one = entity.kdj_k 403 | two = entity.kdj_d 404 | three = entity.kdj_j 405 | }else if self.secondDrawString == KLINERSI { 406 | one = entity.rsi 407 | } 408 | bottomChartTextLayer.configureBottomValue(secondDrawString: secondDrawString, one: one, two: two, three: three) 409 | 410 | // 长按数据 411 | let touchY = point.y 412 | if touchY >= topChartTop && touchY <= midTextTop { 413 | // k线数据 414 | let currentPrice: CGFloat = kLine.mainMinPrice + (midTextTop - touchY - theme.topChartMinYGap) / kLine.priceUnit 415 | 416 | crossLineLayer.moveCrossLineLayer(touchNum: currentPrice, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: nil, dateType: dateType) 417 | 418 | if let kLineViewDelegate = self.kLineViewDelegate { 419 | kLineViewDelegate.XLKLineViewLongPress(model: entity, preClose: preData.close) 420 | } 421 | 422 | }else if touchY > midTextTop && touchY < bottomChartTop { 423 | crossLineLayer.moveCrossLineLayer(touchNum: nil, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: nil, dateType: dateType) 424 | 425 | }else if touchY >= bottomChartTop && touchY <= timeTextTop { 426 | var currentNum: CGFloat = 0 427 | 428 | // 成交量数据 429 | if secondDrawString == KLINEVOL { 430 | if touchY >= bottomChartTop + theme.volumeGap { 431 | currentNum = kLine.minVolume + (timeTextTop - touchY) / kLine.bottomChartUnit 432 | 433 | if currentNum >= kLine.maxVolume { 434 | currentNum = kLine.maxVolume 435 | } 436 | if currentNum <= kLine.minVolume { 437 | currentNum = kLine.minVolume 438 | } 439 | crossLineLayer.moveCrossLineLayer(touchNum: currentNum, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEVOL, dateType: dateType) 440 | }else { 441 | // 超出最高不显示 442 | crossLineLayer.moveCrossLineLayer(touchNum: nil, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEVOL, dateType: dateType) 443 | } 444 | }else if secondDrawString == KLINEMACD { 445 | // MACD 446 | if touchY >= bottomChartTop + theme.volumeGap { 447 | currentNum = kLine.minMACD + (timeTextTop - touchY - theme.volumeGap) / kLine.bottomChartUnit 448 | 449 | if currentNum >= kLine.maxMACD { 450 | currentNum = kLine.maxMACD 451 | } 452 | 453 | crossLineLayer.moveCrossLineLayer(touchNum: currentNum, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEMACD, dateType: dateType) 454 | }else { 455 | // 超出最高不显示 456 | crossLineLayer.moveCrossLineLayer(touchNum: nil, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEMACD, dateType: dateType) 457 | } 458 | }else if secondDrawString == KLINEKDJ { 459 | // DKJ 460 | if touchY >= bottomChartTop + theme.volumeGap { 461 | currentNum = kLine.minKDJ + (timeTextTop - touchY - theme.volumeGap) / kLine.bottomChartUnit 462 | 463 | if currentNum >= kLine.maxKDJ { 464 | currentNum = kLine.maxKDJ 465 | } 466 | 467 | crossLineLayer.moveCrossLineLayer(touchNum: currentNum, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEKDJ, dateType: dateType) 468 | }else { 469 | // 超出最高不显示 470 | crossLineLayer.moveCrossLineLayer(touchNum: nil, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEKDJ, dateType: dateType) 471 | } 472 | }else if secondDrawString == KLINERSI { 473 | // RSI 474 | if touchY >= bottomChartTop + theme.volumeGap { 475 | currentNum = kLine.minRSI + (timeTextTop - touchY - theme.volumeGap) / kLine.bottomChartUnit 476 | 477 | if currentNum >= kLine.maxRSI { 478 | currentNum = kLine.maxRSI 479 | } 480 | 481 | crossLineLayer.moveCrossLineLayer(touchNum: currentNum, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINERSI, dateType: dateType) 482 | }else { 483 | // 超出最高不显示 484 | crossLineLayer.moveCrossLineLayer(touchNum: nil, touchPoint: point, pricePoint: CGPoint(x: centerX, y: highLightClose), volumePoint: CGPoint(x: centerX, y: highLightVolume), model: entity, secondString: KLINEKDJ, dateType: dateType) 485 | } 486 | } 487 | 488 | if let kLineViewDelegate = self.kLineViewDelegate { 489 | kLineViewDelegate.XLKLineViewLongPress(model: entity, preClose: preData.close) 490 | } 491 | } 492 | } 493 | } 494 | 495 | /// 处理点击事件 496 | @objc func handleTapGestureAction(_ recognizer: UITapGestureRecognizer) { 497 | if isLongPressEnd { 498 | hideCross() 499 | }else { 500 | beginShowCross(recognizer) 501 | } 502 | } 503 | 504 | // 长按操作 505 | @objc func handleLongPressGestureAction(_ recognizer: UILongPressGestureRecognizer) { 506 | if recognizer.state == .began || recognizer.state == .changed { 507 | beginShowCross(recognizer) 508 | } 509 | 510 | if recognizer.state == .ended { 511 | isLongPressEnd = true 512 | } 513 | } 514 | 515 | @objc open func hideCross() { 516 | crossLineLayer.isHidden = true 517 | if let kLineViewDelegate = self.kLineViewDelegate { 518 | kLineViewDelegate.XLKLineViewHideCrossDetail() 519 | } 520 | isLongPressEnd = false 521 | } 522 | 523 | // 捏合缩放扩大操作 524 | @objc func handlePinGestureAction(_ recognizer: UIPinchGestureRecognizer) { 525 | 526 | guard recognizer.numberOfTouches == 2 else { return } 527 | 528 | switch recognizer.state { 529 | case .began: 530 | enableKVO = false 531 | scrollView.isScrollEnabled = false 532 | default: 533 | enableKVO = true 534 | scrollView.isScrollEnabled = true 535 | } 536 | 537 | let scale = recognizer.scale 538 | let originScale: CGFloat = 1.0 539 | let kLineScaleFactor: CGFloat = 0.1 540 | let kLineScaleBound: CGFloat = 0.03 541 | let diffScale = scale - originScale // 获取缩放倍数 542 | 543 | if abs(diffScale) > kLineScaleBound { 544 | let point1 = recognizer.location(ofTouch: 0, in: self) 545 | let point2 = recognizer.location(ofTouch: 1, in: self) 546 | 547 | if abs(point1.x - preOneTouchX) < 5, abs(point2.x - preTwoTouchX) < 5 { 548 | return 549 | } 550 | preOneTouchX = point1.x 551 | preTwoTouchX = point2.x 552 | 553 | let pinCenterX = (point1.x + point2.x) / 2 554 | let scrollViewPinCenterX = pinCenterX + scrollView.contentOffset.x 555 | 556 | // 中心点数据index 557 | let pinCenterLeftCount = scrollViewPinCenterX / (theme.candleWidth + theme.candleGap) 558 | 559 | // 缩放后的candle宽度 560 | let newCandleWidth = theme.candleWidth * (recognizer.velocity > 0 ? (1 + kLineScaleFactor) : (1 - kLineScaleFactor)) 561 | 562 | if newCandleWidth > theme.candleMaxWidth { 563 | self.theme.candleWidth = theme.candleMaxWidth 564 | kLine.theme.candleWidth = theme.candleMaxWidth 565 | 566 | } else if newCandleWidth < theme.candleMinWidth { 567 | self.theme.candleWidth = theme.candleMinWidth 568 | kLine.theme.candleWidth = theme.candleMinWidth 569 | 570 | } else { 571 | self.theme.candleWidth = newCandleWidth 572 | kLine.theme.candleWidth = newCandleWidth 573 | } 574 | 575 | // 更新容纳的总长度 576 | self.updateKlineViewWidth() 577 | 578 | let newPinCenterX = pinCenterLeftCount * theme.candleWidth + (pinCenterLeftCount - 1) * theme.candleGap 579 | let newOffsetX = newPinCenterX - pinCenterX 580 | self.scrollView.contentOffset = CGPoint(x: newOffsetX > 0 ? newOffsetX : 0 , y: self.scrollView.contentOffset.y) 581 | 582 | kLine.contentOffsetX = scrollView.contentOffset.x 583 | kLine.drawKLineView(mainDrawString: mainDrawString, secondDrawString: secondDrawString, lineType: lineType) 584 | } 585 | 586 | } 587 | } 588 | 589 | extension XLKLineView: UIScrollViewDelegate { 590 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 591 | hideCross() 592 | // 用于滑动加载更多 KLine 数据 593 | if (scrollView.contentOffset.x == 0) { 594 | preKLineRightOffset = kLineViewWidth + theme.candleWidth 595 | if let kLineViewDelegate = self.kLineViewDelegate { 596 | kLineViewDelegate.XLKLineViewLoadMore() 597 | } 598 | } 599 | } 600 | } 601 | 602 | 603 | 604 | 605 | 606 | 607 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLMidChartTextLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLMidChartTextLayer.swift 3 | // behoo 4 | // 5 | // Created by 夏磊 on 2018/8/23. 6 | // Copyright © 2018年 behoo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// k线视图中部左侧文字layer 12 | class XLMidChartTextLayer: XLCAShapeLayer { 13 | 14 | var theme = XLKLineStyle() 15 | 16 | fileprivate let textWidth: CGFloat = 90 17 | fileprivate let textHeight: CGFloat = 12 18 | fileprivate let textLeftOffset: CGFloat = 4 19 | 20 | var topChartHeight: CGFloat { 21 | get { 22 | return frame.height - theme.topTextHeight - theme.midTextHeight 23 | } 24 | } 25 | 26 | // 横向分隔线间距 27 | var topChartLineMargin: CGFloat { 28 | get { 29 | return self.topChartHeight / 4 30 | } 31 | } 32 | 33 | override func layoutSublayers() { 34 | super.layoutSublayers() 35 | oneText.frame = CGRect(x: textLeftOffset, y: theme.topTextHeight, width: self.textWidth, height: self.textHeight) 36 | 37 | twoText.frame = CGRect(x: textLeftOffset, y: self.topChartLineMargin + self.theme.topTextHeight - self.textHeight, width: self.textWidth, height: self.textHeight) 38 | 39 | threeText.frame = CGRect(x: textLeftOffset, y: self.topChartLineMargin * 2 + self.theme.topTextHeight - self.textHeight, width: self.textWidth, height: self.textHeight) 40 | 41 | fourText.frame = CGRect(x: textLeftOffset, y: self.topChartLineMargin * 3 + self.theme.topTextHeight - self.textHeight, width: self.textWidth, height: self.textHeight) 42 | 43 | fiveText.frame = CGRect(x: textLeftOffset, y: self.theme.topTextHeight + self.topChartHeight - self.textHeight, width: self.textWidth, height: self.textHeight) 44 | 45 | sixText.frame = CGRect(x: textLeftOffset, y: self.theme.topTextHeight + self.topChartHeight + self.theme.midTextHeight, width: self.textWidth, height: self.textHeight) 46 | } 47 | 48 | lazy var oneText: XLCATextLayer = { 49 | let oneText = XLCATextLayer() 50 | oneText.fontSize = 10 51 | oneText.foregroundColor = self.theme.textColor.cgColor 52 | oneText.backgroundColor = UIColor.clear.cgColor 53 | oneText.alignmentMode = .left 54 | oneText.contentsScale = UIScreen.main.scale 55 | return oneText 56 | }() 57 | 58 | lazy var twoText: XLCATextLayer = { 59 | let twoText = XLCATextLayer() 60 | twoText.fontSize = 10 61 | twoText.foregroundColor = self.theme.textColor.cgColor 62 | twoText.backgroundColor = UIColor.clear.cgColor 63 | twoText.alignmentMode = .left 64 | twoText.contentsScale = UIScreen.main.scale 65 | return twoText 66 | }() 67 | 68 | lazy var threeText: XLCATextLayer = { 69 | let threeText = XLCATextLayer() 70 | threeText.fontSize = 10 71 | threeText.foregroundColor = self.theme.textColor.cgColor 72 | threeText.backgroundColor = UIColor.clear.cgColor 73 | threeText.alignmentMode = .left 74 | threeText.contentsScale = UIScreen.main.scale 75 | return threeText 76 | }() 77 | 78 | lazy var fourText: XLCATextLayer = { 79 | let fourText = XLCATextLayer() 80 | fourText.fontSize = 10 81 | fourText.foregroundColor = self.theme.textColor.cgColor 82 | fourText.backgroundColor = UIColor.clear.cgColor 83 | fourText.alignmentMode = .left 84 | fourText.contentsScale = UIScreen.main.scale 85 | return fourText 86 | }() 87 | 88 | lazy var fiveText: XLCATextLayer = { 89 | let fiveText = XLCATextLayer() 90 | fiveText.fontSize = 10 91 | fiveText.foregroundColor = self.theme.textColor.cgColor 92 | fiveText.backgroundColor = UIColor.clear.cgColor 93 | fiveText.alignmentMode = .left 94 | fiveText.contentsScale = UIScreen.main.scale 95 | return fiveText 96 | }() 97 | 98 | lazy var sixText: XLCATextLayer = { 99 | let sixText = XLCATextLayer() 100 | sixText.fontSize = 10 101 | sixText.foregroundColor = self.theme.textColor.cgColor 102 | sixText.backgroundColor = UIColor.clear.cgColor 103 | sixText.alignmentMode = .left 104 | sixText.contentsScale = UIScreen.main.scale 105 | return sixText 106 | }() 107 | 108 | override init() { 109 | super.init() 110 | addSublayer(oneText) 111 | addSublayer(twoText) 112 | addSublayer(threeText) 113 | addSublayer(fourText) 114 | addSublayer(fiveText) 115 | addSublayer(sixText) 116 | } 117 | 118 | required init?(coder aDecoder: NSCoder) { 119 | fatalError("init(coder:) has not been implemented") 120 | } 121 | 122 | func configurePriceVolue(one: CGFloat, two: CGFloat, three: CGFloat, four: CGFloat, five: CGFloat, six: CGFloat) { 123 | 124 | oneText.string = one.xlChart.kLinePriceNumber() 125 | twoText.string = two.xlChart.kLinePriceNumber() 126 | threeText.string = three.xlChart.kLinePriceNumber() 127 | fourText.string = four.xlChart.kLinePriceNumber() 128 | fiveText.string = five.xlChart.kLinePriceNumber() 129 | sixText.string = six.xlChart.kLineVolNumber() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChart/XLStockChart/KLineChart/XLTopChartTextLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLTopChartTextLayer.swift 3 | // XLStockChart 4 | // 5 | // Created by 夏磊 on 2018/8/23. 6 | // Copyright © 2018年 sum123. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// K线顶部 例:MA5 MA10 MA30 文字layer 12 | class XLTopChartTextLayer: XLCAShapeLayer, XLDrawLayerProtocol { 13 | 14 | var theme = XLKLineStyle() 15 | 16 | fileprivate let textHeight: CGFloat = 12 17 | fileprivate let textTop: CGFloat = 1 18 | 19 | // MARK: - Life Cycle 20 | override init() { 21 | super.init() 22 | addSublayer(titleText) 23 | addSublayer(oneText) 24 | addSublayer(twoText) 25 | addSublayer(threeText) 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | // MARK: - Method 33 | func configureTopValue(lineType: XLKLineType, mainDrawString: String, one: CGFloat, two: CGFloat, three: CGFloat) { 34 | 35 | var oneWidth: CGFloat = 0 36 | var twoWidth: CGFloat = 0 37 | var threeWidth: CGFloat = 0 38 | var textMargin: CGFloat = 0 39 | 40 | var titleString = "" 41 | var oneString = "" 42 | var twoString = "" 43 | var threeString = "" 44 | 45 | switch lineType { 46 | case .minLineType: 47 | print("分时") 48 | if mainDrawString == KLINEMA { 49 | // MA 50 | titleText.string = "均线 " 51 | titleText.frame = CGRect(x: 4, y: 0, width: 30, height: self.theme.topTextHeight) 52 | 53 | let oneString = "MA60: " + one.xlChart.kLinePriceNumber() 54 | let oneWidth = getTextSize(text: oneString, addOnWith: 5).width 55 | oneText.string = oneString 56 | oneText.frame = CGRect(x: titleText.frame.maxX, y: textTop, width: oneWidth, height: textHeight) 57 | 58 | titleText.isHidden = false 59 | oneText.isHidden = false 60 | twoText.isHidden = true 61 | threeText.isHidden = true 62 | 63 | }else if mainDrawString == KLINEBOLL { 64 | // BOLL 65 | titleText.string = "BOLL " 66 | titleText.frame = CGRect(x: 4, y: textTop, width: 30, height: self.textHeight) 67 | 68 | let oneString = "MID: " + two.xlChart.kLinePriceNumber() 69 | let oneWidth = getTextSize(text: oneString, addOnWith: 5).width 70 | oneText.string = oneString 71 | oneText.frame = CGRect(x: titleText.frame.maxX, y: textTop, width: oneWidth, height: textHeight) 72 | 73 | titleText.isHidden = false 74 | oneText.isHidden = false 75 | twoText.isHidden = true 76 | threeText.isHidden = true 77 | }else { 78 | titleText.isHidden = true 79 | oneText.isHidden = true 80 | twoText.isHidden = true 81 | threeText.isHidden = true 82 | } 83 | 84 | 85 | case .candleLineType: 86 | // K线 87 | if mainDrawString == KLINEMA { 88 | // MA 89 | titleString = "均线 " 90 | titleText.frame = CGRect(x: 4, y: 0, width: 30, height: self.theme.topTextHeight) 91 | textMargin = 5 92 | 93 | oneString = "MA5: " + one.xlChart.kLinePriceNumber() 94 | twoString = "MA10: " + two.xlChart.kLinePriceNumber() 95 | threeString = "MA30: " + three.xlChart.kLinePriceNumber() 96 | 97 | titleText.string = titleString 98 | oneText.string = oneString 99 | twoText.string = twoString 100 | threeText.string = threeString 101 | 102 | oneWidth = getTextSize(text: oneString, addOnWith: textMargin).width 103 | twoWidth = getTextSize(text: twoString, addOnWith: textMargin).width 104 | threeWidth = getTextSize(text: threeString, addOnWith: textMargin).width 105 | 106 | oneText.frame = CGRect(x: titleText.frame.maxX, y: textTop, width: oneWidth, height: textHeight) 107 | twoText.frame = CGRect(x: oneText.frame.maxX, y: textTop, width: twoWidth, height: textHeight) 108 | threeText.frame = CGRect(x: twoText.frame.maxX, y: textTop, width: threeWidth, height: textHeight) 109 | 110 | titleText.isHidden = false 111 | oneText.isHidden = false 112 | twoText.isHidden = false 113 | threeText.isHidden = false 114 | 115 | }else if mainDrawString == KLINEBOLL { 116 | // BOLL 117 | titleString = "BOLL " 118 | titleText.frame = CGRect(x: 4, y: textTop, width: 30, height: self.textHeight) 119 | textMargin = 5 120 | 121 | oneString = "UP: " + one.xlChart.kLinePriceNumber() 122 | twoString = "MID: " + two.xlChart.kLinePriceNumber() 123 | threeString = "LOW: " + three.xlChart.kLinePriceNumber() 124 | 125 | titleText.string = titleString 126 | oneText.string = oneString 127 | twoText.string = twoString 128 | threeText.string = threeString 129 | 130 | oneWidth = getTextSize(text: oneString, addOnWith: textMargin).width 131 | twoWidth = getTextSize(text: twoString, addOnWith: textMargin).width 132 | threeWidth = getTextSize(text: threeString, addOnWith: textMargin).width 133 | 134 | oneText.frame = CGRect(x: titleText.frame.maxX, y: textTop, width: oneWidth, height: textHeight) 135 | twoText.frame = CGRect(x: oneText.frame.maxX, y: textTop, width: twoWidth, height: textHeight) 136 | threeText.frame = CGRect(x: twoText.frame.maxX, y: textTop, width: threeWidth, height: textHeight) 137 | 138 | titleText.isHidden = false 139 | oneText.isHidden = false 140 | twoText.isHidden = false 141 | threeText.isHidden = false 142 | }else { 143 | titleText.isHidden = true 144 | oneText.isHidden = true 145 | twoText.isHidden = true 146 | threeText.isHidden = true 147 | } 148 | 149 | } 150 | } 151 | 152 | // MARK: - Lazy 153 | lazy var titleText: XLCATextLayer = { 154 | let titleText = XLCATextLayer() 155 | titleText.fontSize = 10 156 | titleText.foregroundColor = self.theme.bottomTextOneColor.cgColor 157 | titleText.backgroundColor = UIColor.clear.cgColor 158 | titleText.alignmentMode = CATextLayerAlignmentMode.left 159 | titleText.contentsScale = UIScreen.main.scale 160 | return titleText 161 | }() 162 | 163 | lazy var oneText: XLCATextLayer = { 164 | let oneText = XLCATextLayer() 165 | oneText.fontSize = 10 166 | oneText.foregroundColor = self.theme.topTextOneColor.cgColor 167 | oneText.backgroundColor = UIColor.clear.cgColor 168 | oneText.alignmentMode = CATextLayerAlignmentMode.left 169 | oneText.contentsScale = UIScreen.main.scale 170 | return oneText 171 | }() 172 | 173 | lazy var twoText: XLCATextLayer = { 174 | let twoText = XLCATextLayer() 175 | twoText.fontSize = 10 176 | twoText.foregroundColor = self.theme.topTextTwoColor.cgColor 177 | twoText.backgroundColor = UIColor.clear.cgColor 178 | twoText.alignmentMode = CATextLayerAlignmentMode.left 179 | twoText.contentsScale = UIScreen.main.scale 180 | return twoText 181 | }() 182 | 183 | lazy var threeText: XLCATextLayer = { 184 | let threeText = XLCATextLayer() 185 | threeText.fontSize = 10 186 | threeText.foregroundColor = self.theme.topTextThreeColor.cgColor 187 | threeText.backgroundColor = UIColor.clear.cgColor 188 | threeText.alignmentMode = CATextLayerAlignmentMode.left 189 | threeText.contentsScale = UIScreen.main.scale 190 | return threeText 191 | }() 192 | } 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChartTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChartTests/XLStockChartTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLStockChartTests.swift 3 | // XLStockChartTests 4 | // 5 | // Created by 夏磊 on 2018/11/20. 6 | // Copyright © 2018 夏磊. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import XLStockChart 11 | 12 | class XLStockChartTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChartUITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /XLStockChart/XLStockChartUITests/XLStockChartUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLStockChartUITests.swift 3 | // XLStockChartUITests 4 | // 5 | // Created by 夏磊 on 2018/11/20. 6 | // Copyright © 2018 夏磊. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class XLStockChartUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sum123/XLStockChart/fcd29980c12c2cf61f8391ebf016f8145dcaaa3c/demo.gif --------------------------------------------------------------------------------