├── _config.yml ├── Examples ├── patternLock.png ├── AppDelegate.swift ├── Info.plist ├── Base.lproj │ └── LaunchScreen.storyboard ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json └── ViewController.swift ├── .gitignore ├── SPPatternLock.podspec ├── LICENSE ├── README.md ├── Sources ├── Components.swift └── LockScreen.swift └── SPPatternLock.xcodeproj └── project.pbxproj /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /Examples/patternLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freesuraj/SPPatternLock/HEAD/Examples/patternLock.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /SPPatternLock.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SPPatternLock' 3 | s.version = '2.0.2' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'Simple and elegant Pattern Lock for iOS' 6 | s.social_media_url = 'http://twitter.com/iosCook' 7 | s.homepage = 'https://github.com/freesuraj/SPPatternLock' 8 | s.authors = { 'Suraj Pathak' => 'freesuraj@gmail.com' } 9 | s.source = { :git => 'https://github.com/freesuraj/SPPatternLock.git', :tag => s.version } 10 | s.ios.deployment_target = '9.0' 11 | s.source_files = 'Sources/*swift' 12 | end -------------------------------------------------------------------------------- /Examples/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SPPatternLock 4 | // 5 | // Created by Suraj Pathak on 14/2/17. 6 | // Copyright © 2017 Suraj. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.rootViewController = UINavigationController(rootViewController: ViewController()) 20 | window?.makeKeyAndVisible() 21 | 22 | return true 23 | } 24 | 25 | 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Suraj Pathak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the Software), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | Permission is hereby granted,. 20 | -------------------------------------------------------------------------------- /Examples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Examples/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 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/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 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/SPPatternLock.svg)](http://cocoadocs.org/docsets/SPPatternLock/) 2 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 3 | [![Platform](https://img.shields.io/cocoapods/p/SPPatternLock.svg?style=flat)](http://cocoadocs.org/docsets/SPPatternLock) 4 | [![Twitter](https://img.shields.io/badge/twitter-@iosCook-blue.svg?style=flat)](http://twitter.com/iosCook) 5 | [![GitHub stars](https://img.shields.io/github/stars/freesuraj/patternlock.svg?style=social&label=Star)](https://github.com/freesuraj/SPPatternlock) 6 | [![GitHub forks](https://img.shields.io/github/forks/freesuraj/patternlock.svg?style=social&label=Fork)](https://github.com/freesuraj/SPPatternlock) 7 | 8 | Pattern Lock for iOS 9 | ======================== 10 | Revamped PatternLock for iOS written in Swift3. 11 | 12 | ![ScreenShot](https://github.com/freesuraj/SPPatternLock/blob/master/Examples/patternLock.png?raw=true) 13 | 14 | 15 | * **no graphics or images** 16 | * **all colors and sizes are customizable** 17 | * **can enable closed-type(complex type) patterns** 18 | * **support for both iPhone and iPad** 19 | * **Easy to bring in and use** 20 | 21 | ### CocoaPods 22 | pod 'SPPatternLock' 23 | ### Initialization 24 | 25 | ```swift 26 | /** 27 | Initializes the main lock screen 28 | 29 | - parameter frame: `CGRect` where the screen will be drawn 30 | - parameter size: Size of the lock screen. It will create grids of size X size. Default value is 3 31 | - parameter allowClosedPattern: If set to `true`, it allows for complicated pattern. Otherwise a circle can't be used twice for a pattern 32 | - parameter config: Configuration for colors and line width, etc 33 | - parameter handler: Callback to receive the user pattern 34 | - returns: Returns the Lock screen 35 | */ 36 | convenience init(frame: CGRect, size: Int = 3, allowClosedPattern: Bool = true, config: Config = Config(), handler: PatternHandlerBlock? = nil) 37 | 38 | ``` 39 | 40 | 41 | ### About 42 | 43 | If you found this little tool useful, I'd love to hear about it. You can also follow me on Twitter at [@realSurajP](https://twitter.com/realSurajP) 44 | 45 | 46 | [![GitHub followers](https://img.shields.io/github/followers/freesuraj.svg?style=social&label=Follow)](https://github.com/freesuraj) 47 | -------------------------------------------------------------------------------- /Examples/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SPPatternLock 4 | // 5 | // Created by Suraj Pathak on 14/2/17. 6 | // Copyright © 2017 Suraj. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var lockScreenView: LockScreen! 14 | 15 | lazy var slider: UISlider = { 16 | let _slider = UISlider(frame: CGRect(x: 100, y: 60, width: self.view.frame.width - 120, height: 50)) 17 | _slider.minimumValue = 3 18 | _slider.maximumValue = 7 19 | _slider.addTarget(self, action: #selector(onDrag), for: .valueChanged) 20 | return _slider 21 | }() 22 | 23 | lazy var complexSwitch: UISwitch = { 24 | let _switch = UISwitch(frame: CGRect(x: 200, y: 120, width: 40, height: 50)) 25 | _switch.addTarget(self, action: #selector(onSwitch), for: .valueChanged) 26 | _switch.isOn = true 27 | return _switch 28 | }() 29 | 30 | private var currentSize: Int = 3 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | let labelSlider = UILabel(frame: CGRect(x: 10, y: 80, width: 100, height: 30)) 36 | labelSlider.text = "Grid size" 37 | labelSlider.sizeToFit() 38 | view.addSubview(labelSlider) 39 | view.addSubview(slider) 40 | 41 | let labelSwitch = UILabel(frame: CGRect(x: 10, y: 130, width: 100, height: 30)) 42 | labelSwitch.text = "Allow Complex" 43 | labelSwitch.sizeToFit() 44 | view.addSubview(labelSwitch) 45 | view.addSubview(complexSwitch) 46 | 47 | title = "SPPatternLock - version 2" 48 | view.backgroundColor = UIColor.lightGray 49 | 50 | updateLockScreen(withSize: currentSize, allowComplex: true) 51 | } 52 | 53 | func updateLockScreen(withSize size: Int, allowComplex: Bool) { 54 | title = "\(size)x\(size) pattern lock" 55 | if let v = lockScreenView { v.removeFromSuperview() } 56 | let lockFrame = CGRect(origin: CGPoint(x: 0, y: complexSwitch.frame.maxY+10), size: CGSize(width: view.frame.width, height: view.frame.width)) 57 | // Example of using config 58 | var config = LockScreen.Config() 59 | config.lineColor = UIColor.purple 60 | lockScreenView = LockScreen(frame: lockFrame, size: size, allowClosedPattern: allowComplex, config: config) { [weak self] (pattern, order) in 61 | print(order.description) 62 | print(pattern) 63 | self?.title = "\(pattern)" 64 | } 65 | view.addSubview(lockScreenView) 66 | } 67 | 68 | override func didReceiveMemoryWarning() { 69 | super.didReceiveMemoryWarning() 70 | // Dispose of any resources that can be recreated. 71 | } 72 | 73 | func onSwitch(sender: AnyClass) { 74 | updateLockScreen(withSize: currentSize, allowComplex: complexSwitch.isOn) 75 | } 76 | 77 | func onDrag(sender: AnyClass) { 78 | let size = Int(slider.value) 79 | if size != currentSize { 80 | updateLockScreen(withSize: size, allowComplex: complexSwitch.isOn) 81 | currentSize = size 82 | } 83 | } 84 | 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /Sources/Components.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Components.swift 3 | // SPPatternLock 4 | // 5 | // Created by Suraj Pathak on 14/2/17. 6 | // Copyright © 2017 Suraj. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreGraphics 11 | 12 | /// A customizable circle that aligned together form the input for pattern lock 13 | class Circle: UIView { 14 | 15 | // *** Customizable attributes *** 16 | var outerColor = UIColor.magenta { 17 | didSet { setNeedsDisplay() } 18 | } 19 | 20 | var innercolor = UIColor.darkGray { 21 | didSet { setNeedsDisplay() } 22 | } 23 | 24 | var highlightColor = UIColor.yellow { 25 | didSet { setNeedsDisplay() } 26 | } 27 | 28 | var lineWidth: CGFloat = 5 { 29 | didSet { setNeedsDisplay() } 30 | } 31 | 32 | /// If selected is `true`, the circle will appear as filled with a smaller circle 33 | var isSelected: Bool { 34 | didSet { setNeedsDisplay() } 35 | } 36 | 37 | override init(frame: CGRect) { 38 | self.isSelected = false 39 | super.init(frame: frame) 40 | } 41 | 42 | convenience init(radius: CGFloat) { 43 | let frame = CGRect(origin: .zero, size: CGSize(width: radius*2, height: radius*2)) 44 | self.init(frame: frame) 45 | backgroundColor = UIColor.clear 46 | } 47 | 48 | required init?(coder aDecoder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | 52 | override func draw(_ rect: CGRect) { 53 | let ctx = UIGraphicsGetCurrentContext() 54 | // Outermost circle 55 | let outerRect = CGRect(origin: CGPoint(x: rect.origin.x + lineWidth, y: rect.origin.y + lineWidth), size: CGSize(width: rect.size.width - 2*lineWidth, height: rect.height - 2*lineWidth)) 56 | ctx?.setLineWidth(lineWidth) 57 | ctx?.setStrokeColor(outerColor.cgColor) 58 | ctx?.fillEllipse(in: outerRect) 59 | // Second circle 60 | let innerRect = outerRect.insetBy(dx: 1, dy: 1) 61 | ctx?.setFillColor(innercolor.cgColor) 62 | ctx?.fillEllipse(in: innerRect) 63 | // For selected circles, the third circle 64 | if isSelected { 65 | let highlightRect = outerRect.insetBy(dx: 10, dy: 10) 66 | ctx?.setFillColor(highlightColor.cgColor) 67 | ctx?.fillEllipse(in: highlightRect) 68 | } 69 | } 70 | 71 | } 72 | 73 | /// A Line represents the line that will be drawn on the canvas when user drags around on the pattern lock 74 | struct Line { 75 | 76 | var fromPoint: CGPoint 77 | var toPoint: CGPoint 78 | /// Boolean to indicate if the line is a full edge or a partial edge 79 | var isFullLength: Bool 80 | 81 | init(fromPoint: CGPoint, toPoint: CGPoint, isFullLength: Bool) { 82 | self.fromPoint = fromPoint 83 | self.toPoint = toPoint 84 | self.isFullLength = isFullLength 85 | } 86 | 87 | } 88 | 89 | /// A pattern view represents the drawing of bunch of lines on the canvas. It's composed of a number of lines 90 | class PatternView: UIView { 91 | var lines: [Line] { 92 | didSet { setNeedsDisplay() } 93 | } 94 | 95 | var lineWidth: CGFloat = 5 { 96 | didSet { setNeedsDisplay() } 97 | } 98 | 99 | var lineColor: UIColor = UIColor.green { 100 | didSet { setNeedsDisplay() } 101 | } 102 | 103 | var linePointColor: UIColor = UIColor.white { 104 | didSet { setNeedsDisplay() } 105 | } 106 | 107 | override init(frame: CGRect) { 108 | lines = [] 109 | super.init(frame: frame) 110 | backgroundColor = UIColor.clear 111 | } 112 | 113 | required init?(coder aDecoder: NSCoder) { 114 | fatalError("init(coder:) has not been implemented") 115 | } 116 | 117 | override func draw(_ rect: CGRect) { 118 | let ctx = UIGraphicsGetCurrentContext() 119 | ctx?.setLineWidth(lineWidth) 120 | ctx?.setStrokeColor(lineColor.cgColor) 121 | for line in lines { 122 | ctx?.move(to: line.fromPoint) 123 | ctx?.addLine(to: line.toPoint) 124 | ctx?.strokePath() 125 | let bubbleWidth = lineWidth // The top and end of the line contains a circular bubble for better UI feel 126 | let frontBubbleFrame = CGRect(origin: CGPoint(x: line.fromPoint.x - bubbleWidth, y: line.fromPoint.y - bubbleWidth), size: CGSize(width: bubbleWidth * 2, height: bubbleWidth * 2)) 127 | ctx?.setFillColor(linePointColor.cgColor) 128 | ctx?.fillEllipse(in: frontBubbleFrame) 129 | 130 | if line.isFullLength { 131 | let backBubbleFrame = CGRect(origin: CGPoint(x: line.toPoint.x - bubbleWidth, y: line.toPoint.y - bubbleWidth), size: CGSize(width: bubbleWidth * 2, height: bubbleWidth * 2)) 132 | ctx?.setFillColor(linePointColor.cgColor) 133 | ctx?.fillEllipse(in: backBubbleFrame) 134 | } 135 | } 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /Sources/LockScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockScreen.swift 3 | // SPPatternLock 4 | // 5 | // Created by Suraj Pathak on 14/2/17. 6 | // Copyright © 2017 Suraj. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class LockScreen: UIView { 12 | 13 | private let kSeed = 23 14 | private let kAlter1 = 1234 15 | private let kAlter2 = 4321 16 | private let kTagId = 222333 17 | 18 | private var selectedCircle: Circle? 19 | private var patternView: PatternView! 20 | private var oldCellIndex: Int = -1 21 | private var currentCellIndex: Int = -1 22 | private var drawnLines: [Int] = [] 23 | private var finalLines: [Line] = [] 24 | private var cellsInOrder: [Int] = [] 25 | 26 | private var allowClosedPattern: Bool = true 27 | 28 | private var size: Int = 3 29 | 30 | private var numberOfCircles: Int { 31 | return size*size 32 | } 33 | 34 | // Public configurable values 35 | public struct Config { 36 | public var lineWidth: CGFloat = 5 37 | public var lineColor: UIColor = UIColor.white 38 | public var lineEdgeColor: UIColor = UIColor.white 39 | public var circleOuterRingColor: UIColor = UIColor.magenta 40 | public var circleInnerRingColor: UIColor = UIColor.darkGray 41 | public var circleHighlightColor: UIColor = UIColor.yellow 42 | 43 | // Public Initializer for Config 44 | public init() {} 45 | } 46 | 47 | public var config: Config = Config() 48 | 49 | public required override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | } 52 | 53 | public typealias PatternHandlerBlock = ((Double,[Int]) -> Void) 54 | var patternHandler: PatternHandlerBlock? 55 | 56 | /** 57 | Initializes the main lock screen 58 | 59 | - parameter frame: `CGRect` where the screen will be drawn 60 | - parameter size: Size of the lock screen. It will create grids of size X size. Default value is 3 61 | - parameter allowClosedPattern: If set to `true`, it allows for complicated pattern. Otherwise a circle can't be used twice for a pattern 62 | - parameter config: Configuration for colors and line width, etc 63 | - parameter handler: Callback to receive the user pattern 64 | - returns: Returns the Lock screen 65 | */ 66 | public convenience init(frame: CGRect, size: Int = 3, allowClosedPattern: Bool = true, config: Config = Config(), handler: PatternHandlerBlock? = nil) { 67 | self.init(frame: frame) 68 | self.size = size 69 | self.allowClosedPattern = allowClosedPattern 70 | self.config = config 71 | self.patternHandler = handler 72 | setNeedsDisplay() 73 | setupScreen() 74 | setupGestures() 75 | } 76 | 77 | required public init?(coder aDecoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | func setupScreen() { 82 | let grid = Double(min(frame.width, frame.height))/Double(2*size+1) 83 | let gap = grid 84 | let topOffset = grid 85 | let radius = grid/2 86 | 87 | for index in 0.. 0 { endPattern() } 123 | else { resetScreen() } 124 | } else { 125 | handlePan(at: point) 126 | } 127 | } else { 128 | let cellPosition = index(point) 129 | oldCellIndex = currentCellIndex 130 | if cellPosition >= 0 { 131 | cellsInOrder.append(currentCellIndex) 132 | perform(#selector(endPattern), with: nil, afterDelay: 0.3) 133 | } 134 | } 135 | } 136 | 137 | /// MARK: Helpers 138 | 139 | func endPattern() { 140 | patternHandler?(uniqueIdOfCurrentPattern,cellsInOrder) 141 | resetScreen() 142 | } 143 | 144 | func cell(at index: Int) -> Circle? { 145 | guard index >= 0 && index < numberOfCircles else { return nil } 146 | return viewWithTag((index/size+kSeed)*kTagId+index % size + kSeed) as? Circle 147 | } 148 | 149 | func index(of circle: Circle) -> Int { 150 | return (circle.tag/kTagId - kSeed)*size + (circle.tag % kTagId - kSeed) 151 | } 152 | 153 | func index(_ point: CGPoint) -> Int { 154 | for view in self.subviews { 155 | if let circle = view as? Circle, circle.frame.contains(point) { 156 | if circle.isSelected == false { 157 | circle.isSelected = true 158 | currentCellIndex = index(of: circle) 159 | selectedCircle = circle 160 | } else if circle.isSelected == true && allowClosedPattern == true { 161 | currentCellIndex = index(of: circle) 162 | selectedCircle = circle 163 | } 164 | 165 | let row = circle.tag/kTagId - kSeed 166 | let col = circle.tag % kTagId - kSeed 167 | return row * size + col 168 | } 169 | } 170 | return -1 171 | } 172 | 173 | func handlePan(at point: CGPoint) { 174 | oldCellIndex = currentCellIndex 175 | let cellPos = index(point) // This part will also change currentCellIndex 176 | if cellPos >= 0 && cellPos != oldCellIndex && allowClosedPattern == true { 177 | 178 | cellsInOrder.append(currentCellIndex) 179 | 180 | }else if cellPos >= 0 && cellPos != oldCellIndex && allowClosedPattern == false && cellsInOrder.count < size*size{ 181 | 182 | cellsInOrder.append(currentCellIndex) 183 | 184 | } 185 | print(finalLines) 186 | if cellPos < 0 && oldCellIndex < 0 { 187 | return 188 | } else if cellPos < 0, let circle = cell(at: oldCellIndex) { 189 | let line = Line(fromPoint: circle.center, toPoint: point, isFullLength: false) 190 | patternView.lines = [] 191 | patternView.lines.append(contentsOf: finalLines) 192 | patternView.lines.append(line) 193 | patternView.setNeedsDisplay() 194 | } else if cellPos >= 0 && currentCellIndex == oldCellIndex { 195 | return 196 | } else if cellPos >= 0 && oldCellIndex == -1 { 197 | return 198 | } else if cellPos >= 0 && oldCellIndex != currentCellIndex { 199 | let uniqueId = uniqueLineIdJoining(cellA: oldCellIndex, cellB: currentCellIndex) 200 | if drawnLines.index(of: uniqueId) == nil, let circle = cell(at: oldCellIndex), let selected = selectedCircle { 201 | let line = Line(fromPoint: circle.center, toPoint: selected.center, isFullLength: true) 202 | finalLines.append(line) 203 | patternView.lines = [] 204 | patternView.lines.append(contentsOf: finalLines) 205 | drawnLines.append(uniqueId) 206 | } 207 | } else { 208 | return 209 | } 210 | 211 | } 212 | 213 | func uniqueLineIdJoining(cellA: Int, cellB: Int) -> Int { 214 | return abs(cellA+cellB)*kAlter1 + abs(cellA-cellB)*kAlter2 215 | } 216 | 217 | var uniqueIdOfCurrentPattern: Double { 218 | var finalNum = 0.0 219 | for index in 0..