├── .gitignore ├── .travis.yml ├── DistancePicker.h ├── DistancePicker.podspec ├── DistancePicker.swift ├── DistancePicker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── DistancePicker.xcscheme │ └── DistancePickerExample.xcscheme ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Brand Assets.launchimage │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── Info.plist ├── LICENSE ├── NEWS.md └── README.md /.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 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | 67 | PodTestInstall/ 68 | 69 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | notifications: 3 | email: 4 | recipients: 5 | - quentin.mathe@gmail.com 6 | xcode_project: DistancePicker.xcodeproj 7 | xcode_scheme: DistancePicker 8 | osx_image: xcode10.2 9 | script: xcodebuild -project DistancePicker.xcodeproj -scheme DistancePicker build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO 10 | -------------------------------------------------------------------------------- /DistancePicker.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Quentin Mathe 3 | 4 | Date: August 2016 5 | License: MIT 6 | */ 7 | 8 | #import 9 | 10 | //! Project version number for DistancePicker. 11 | FOUNDATION_EXPORT double DistancePickerVersionNumber; 12 | 13 | //! Project version string for DistancePicker. 14 | FOUNDATION_EXPORT const unsigned char DistancePickerVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | -------------------------------------------------------------------------------- /DistancePicker.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "DistancePicker" 4 | s.version = "0.8.4" 5 | s.summary = "UIKit control to select a distance with a pan gesture, written in Swift" 6 | s.description = "DistancePicker is a custom UIKit control to select a distance with a pan gesture. It looks like a ruler with multiple distance marks and can be used to resize a map, set up a geofence or choose a search radius." 7 | s.homepage = "https://github.com/qmathe/DistancePicker" 8 | s.screenshots = "http://www.quentinmathe.com/github/DistancePicker/Add%20Place%20with%20Search%20Radius%20-%20iPhone%205.jpg" 9 | s.license = "MIT" 10 | s.author = { "Quentin Mathé" => "quentin.mathe@gmail.com" } 11 | s.social_media_url = "http://twitter.com/quentin_mathe" 12 | 13 | s.swift_version = "5.0" 14 | s.platform = :ios, "8.0" 15 | 16 | s.source = { :git => "https://github.com/qmathe/DistancePicker.git" } 17 | s.source_files = '*.swift', '*.{h,m}' 18 | s.public_header_files = "*.h" 19 | s.framework = "MapKit" 20 | 21 | end 22 | -------------------------------------------------------------------------------- /DistancePicker.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2014 Quentin Mathe 3 | 4 | Date: November 2014 5 | License: MIT 6 | */ 7 | 8 | import Foundation 9 | import UIKit 10 | import MapKit 11 | 12 | 13 | open class DistancePicker : UIControl, UIDynamicAnimatorDelegate { 14 | 15 | // MARK: - Cached State 16 | 17 | open var formatter: MKDistanceFormatter = { 18 | let formatter = MKDistanceFormatter() 19 | formatter.unitStyle = .abbreviated 20 | return formatter 21 | }() 22 | 23 | // MARK: - Target/Action State 24 | 25 | open weak var target: AnyObject? 26 | open var action: Selector? 27 | 28 | // MARK: - Content State 29 | 30 | open var marks: [Double] = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 5000, 10000, 20000, 30000, 50000, 100000, 200000, .greatestFiniteMagnitude] { 31 | didSet { 32 | formattedMarks = formattedMarksFromMarks(marks) 33 | } 34 | } 35 | open lazy var formattedMarks: [String] = formattedMarksFromMarks(marks) 36 | open var usesMetricSystem: Bool = shouldUseMetricSystem() { 37 | didSet { 38 | formatter.units = usesMetricSystem ? .metric : .imperial 39 | formattedMarks = formattedMarksFromMarks(marks) 40 | } 41 | } 42 | 43 | // MARK: - Appearance State 44 | 45 | private let markParagraphStyle: NSMutableParagraphStyle = { 46 | let style = NSMutableParagraphStyle() 47 | style.alignment = .center 48 | return style 49 | }() 50 | private let markFont = UIFont(name: "Avenir-Medium", size: 13) ?? UIFont.systemFont(ofSize: 11) 51 | // Due to a bug in Swift 2.2, we have to call init explicity. 52 | // 53 | // Was using systemFontOfSize(11) previously. 54 | open lazy var markAttributes: [NSAttributedString.Key: Any] = [ 55 | .font: markFont, 56 | .paragraphStyle: markParagraphStyle, 57 | .foregroundColor: UIColor.gray.withAlphaComponent(0.8) 58 | ] 59 | open var markSpacing = CGFloat(50) 60 | open var markColor = UIColor.lightGray 61 | open var numberOfIncrementsBetweenMarks = 5 62 | open var incrementSpacing: CGFloat { 63 | return markSpacing / CGFloat(numberOfIncrementsBetweenMarks) 64 | } 65 | open var incrementColor = UIColor.lightGray.withAlphaComponent(0.5) 66 | open var markLineLength: CGFloat { 67 | return markSpacing * CGFloat(marks.count - 1) 68 | } 69 | 70 | // MARK: - Selection State 71 | 72 | // The selected position on the mark line that starts with zero and ends 73 | // with -markLineLength. 74 | // This position usually falls between two mark positions. We can extract 75 | // the lower and upper marks with floor(), ceil() or round() to round it 76 | // towards the closest mark. 77 | open var selectedPosition: CGFloat { 78 | return bounds.width * 0.5 - offset 79 | } 80 | open var selectedMarkIndex: Int { 81 | return previousMarkIndex 82 | } 83 | // The index of the mark before the selected point 84 | fileprivate var previousMarkIndex: Int { 85 | let markIndex = Int(floor(selectedPosition / markSpacing)) 86 | return markIndex > (marks.count - 1) ? marks.count - 1 : markIndex 87 | } 88 | // The index of the mark after the selected point 89 | fileprivate var nextMarkIndex: Int { 90 | let markIndex = Int(ceil(selectedPosition / markSpacing)) 91 | return markIndex > (marks.count - 1) ? marks.count - 1 : markIndex 92 | } 93 | open var selectedMark: Double { 94 | //print("Selected mark index \(selectedMarkIndex)") 95 | return marks[selectedMarkIndex] 96 | } 97 | open var selectedFormattedMark: String { 98 | return formattedMarks[selectedMarkIndex] 99 | } 100 | open var selectedIncrementIndex: Int { 101 | let selectedMarkPosition = CGFloat(selectedMarkIndex) * markSpacing 102 | let positionFromMark = selectedPosition - selectedMarkPosition 103 | let incrementIndex = Int(round(positionFromMark / incrementSpacing)) 104 | 105 | return incrementIndex > (numberOfIncrementsBetweenMarks - 1) ? numberOfIncrementsBetweenMarks - 1 : incrementIndex 106 | } 107 | open var selectedIncrement: Double { 108 | // When the next mark is the last one (infinite), we use a 1000 km as 109 | // our last mark value, to compute the increment on a range between 110 | // 200 km and 1000 km. 111 | let maxMarkForIncrement = Double(1000000) 112 | let nextMark = marks[nextMarkIndex] == .greatestFiniteMagnitude ? maxMarkForIncrement : marks[nextMarkIndex] 113 | let previousMark = marks[previousMarkIndex] == .greatestFiniteMagnitude ? maxMarkForIncrement : marks[previousMarkIndex] 114 | let incrementTotal = nextMark - previousMark 115 | //print("Increment total \(incrementTotal)") 116 | assert(incrementTotal >= 0 && incrementTotal <= maxMarkForIncrement) 117 | let incrementValue = incrementTotal != 0 ? incrementTotal / Double(numberOfIncrementsBetweenMarks) : Double(0) 118 | 119 | return Double(selectedIncrementIndex) * incrementValue 120 | } 121 | open var selectedValue: Double { 122 | //print("Selected mark \(selectedMark) increment \(selectedIncrement)") 123 | return selectedMark == .greatestFiniteMagnitude ? selectedMark : selectedMark + selectedIncrement 124 | } 125 | 126 | // MARK: - Geometry State 127 | 128 | // A zero offset doesn't represent the lowest mark, since the head that 129 | // selects a mark is not at the picker origin but at the bounds width middle. 130 | // So the selected mark for the zero offset depends on the screen size. 131 | // 132 | // At zero, the lowest is the bounds origin. 133 | // 134 | // When the offset increases, the selected mark index/value decreases. 135 | // When the offset descreases, the selected mark index/value increases. 136 | // 137 | // The offset doesn't shift the picker bounds origin towards the left or 138 | // right, since the picker head must always be drawn in the center and 139 | // moving the bounds would move the picker head (putting the picker head 140 | // in another layer is not so helpful since we must redraw the selected 141 | // increment and mark with the tint color too) 142 | open var offset: CGFloat = CGFloat(0) { 143 | didSet { 144 | if offset > maxOffset { 145 | offset = maxOffset 146 | } 147 | else if offset < minOffset { 148 | offset = minOffset 149 | } 150 | //print("\(offset) min: \(minOffset) max: \(maxOffset)") 151 | setNeedsDisplay() 152 | } 153 | } 154 | // The minimum offset corresponds to the infinite mark, since we move the 155 | // picker to the left (the offset grows towards negative numbers). 156 | open var minOffset: CGFloat { 157 | return -(markLineLength - (bounds.size.width * 0.5)) 158 | } 159 | // The maximum offset corresponds to the zero mark, since we move the 160 | // picker to the right (the offset grows towards positive numbers). 161 | open var maxOffset: CGFloat { 162 | return bounds.size.width * 0.5 163 | } 164 | fileprivate var normalizedBounds: CGRect { 165 | var normalizedBounds = bounds 166 | normalizedBounds.size.width = 1000 167 | return normalizedBounds 168 | } 169 | // An offset that can be saved and reloaded indepently of the distance 170 | // picker bounds (e.g. when the screen is rotated or bigger/smaller) 171 | open var normalizedOffset: CGFloat { 172 | get { 173 | return convertOffset(offset, fromBounds: bounds, toBounds: normalizedBounds) 174 | } 175 | set { 176 | offset = convertOffset(newValue, fromBounds: normalizedBounds, toBounds: bounds) 177 | } 178 | } 179 | // Adjust the offset on resizing (this means screen rotation is supported) 180 | override open var bounds: CGRect { 181 | didSet { 182 | offset = convertOffset(offset, fromBounds: oldValue, toBounds: bounds) 183 | } 184 | } 185 | // Fallback to cover the case where DistancePicker.bounds.set isn't called on resizing (prior to iOS 10) 186 | override open var frame: CGRect { 187 | didSet { 188 | offset = convertOffset(offset, fromBounds: oldValue, toBounds: frame) 189 | } 190 | } 191 | 192 | open func convertOffset(_ offset: CGFloat, 193 | fromBounds oldBounds: CGRect, 194 | toBounds newBounds: CGRect) -> CGFloat { 195 | return offset + (newBounds.size.width - oldBounds.size.width) / 2 196 | } 197 | 198 | // MARK: - Animation State 199 | 200 | fileprivate var dynamicItem = DynamicItem() 201 | open lazy var animator: UIDynamicAnimator = { 202 | let animator = UIDynamicAnimator(referenceView: self) 203 | animator.delegate = self 204 | return animator 205 | }() 206 | 207 | open func formattedMarksFromMarks(_ marks: [Double]) -> [String] { 208 | // For non-metric system, here is how we interpret the base marks in the 209 | // imperial system: 210 | // 211 | // - 100 m to 0.1 mi (~ 500 ft or ~ 150 m) 212 | // - 200 m to 0.2 mi (~ 1000 ft or ~ 300 m) 213 | // 214 | // This ensures the proposed distances increase in a regular manner with 215 | // numbers that don't appear too random (when comparing miles to meters 216 | // that get used by default.) 217 | // 218 | // Note: there is no unit attached to the 'marks' property initially. 219 | let meterMarks = usesMetricSystem ? marks : marks.map { 220 | let miles = $0 == .greatestFiniteMagnitude ? .greatestFiniteMagnitude : $0 / 1000 221 | 222 | return metersFromMiles(miles) 223 | } 224 | 225 | return meterMarks.map { 226 | if $0 == .greatestFiniteMagnitude { 227 | return "∞" 228 | } 229 | 230 | // Distance argument must be in meters 231 | return self.formatter.string(fromDistance: $0 as CLLocationDistance) 232 | } 233 | } 234 | 235 | // MARK: - Initialization 236 | 237 | fileprivate func setUp() { 238 | addGestureRecognizer(PanGestureRecognizer(target: self, action: #selector(DistancePicker.pan(_:)))) 239 | } 240 | 241 | override public init(frame: CGRect) { 242 | super.init(frame: frame) 243 | setUp() 244 | backgroundColor = UIColor.white 245 | } 246 | 247 | required public init?(coder aDecoder: NSCoder) { 248 | super.init(coder: aDecoder) 249 | setUp() 250 | } 251 | 252 | // MARK: - Event Handling 253 | 254 | open func decelerationBehaviorWithVelocity(_ velocity: CGPoint) -> UIDynamicItemBehavior { 255 | let inverseVelocity = CGPoint(x: velocity.x, y: 0) 256 | let decelerationBehavior = UIDynamicItemBehavior(items: [dynamicItem]) 257 | 258 | dynamicItem.center = CGPoint(x: offset, y: 0) 259 | 260 | decelerationBehavior.addLinearVelocity(inverseVelocity, for: dynamicItem); 261 | decelerationBehavior.resistance = 4.0 262 | decelerationBehavior.action = { 263 | self.offset = self.dynamicItem.center.x 264 | } 265 | 266 | return decelerationBehavior 267 | } 268 | 269 | @IBAction open func pan(_ recognizer: UIPanGestureRecognizer) { 270 | let velocity = recognizer.velocity(in: self) 271 | 272 | if recognizer.state == UIGestureRecognizer.State.began { 273 | animator.removeAllBehaviors() 274 | } 275 | else if recognizer.state == UIGestureRecognizer.State.changed { 276 | assert(animator.behaviors.isEmpty) 277 | 278 | offset += recognizer.translation(in: self).x 279 | recognizer.setTranslation(.zero, in: self) 280 | } 281 | else if recognizer.state == .ended { 282 | assert(animator.behaviors.isEmpty) 283 | 284 | animator.addBehavior(decelerationBehaviorWithVelocity(velocity)) 285 | } 286 | } 287 | 288 | // MARK: Drawing 289 | 290 | override open func draw(_ rect: CGRect) { 291 | var position = CGPoint(x: offset, y: 0) 292 | 293 | for mark in formattedMarks { 294 | let tickMarkEndPoint = CGPoint(x: position.x, y: position.y + 7) 295 | let markValueRect = markValueRectForPosition(position) 296 | var attributes = markAttributes 297 | 298 | self.drawLineFrom(position, 299 | to: tickMarkEndPoint, 300 | withColor: markColor) 301 | 302 | if (selectedFormattedMark == mark) { 303 | let selectedFont = UIFont(name: "Avenir-Heavy", size: markFont.pointSize) ?? UIFont.boldSystemFont(ofSize: markFont.pointSize) 304 | 305 | attributes[.foregroundColor] = tintColor 306 | attributes[.font] = selectedFont 307 | 308 | // To compute a corrected center: 309 | //markValueRect.origin.y -= (selectedFont.capHeight - font.capHeight) / 2 310 | } 311 | 312 | mark.draw(in: markValueRect, withAttributes: attributes) 313 | 314 | if formattedMarks.last != mark { 315 | drawMarkIncrementsFromPosition(position) 316 | } 317 | 318 | position.x += markSpacing 319 | } 320 | 321 | drawLineFrom(CGPoint(x: bounds.width * 0.5, y: position.y), 322 | to: CGPoint(x: bounds.width * 0.5, y: position.y + 10), 323 | withColor: tintColor) 324 | } 325 | 326 | open func drawMarkIncrementsFromPosition(_ startPosition: CGPoint) { 327 | let incrementSpacing = markSpacing / CGFloat(numberOfIncrementsBetweenMarks) 328 | var position = CGPoint(x: startPosition.x + incrementSpacing, y: startPosition.y) 329 | 330 | for _ in 1.. CGRect { 342 | let maxWidth = markSpacing 343 | let maxHeight = CGFloat.greatestFiniteMagnitude 344 | // Was using 12 previously 345 | let yOffset: CGFloat = 16 346 | 347 | return CGRect(x: position.x - maxWidth * 0.5, 348 | y: position.y + yOffset, 349 | width: maxWidth, 350 | height: maxHeight) 351 | } 352 | 353 | open func drawLineFrom(_ startPoint: CGPoint, to endPoint: CGPoint, withColor color: UIColor) { 354 | let line = UIBezierPath() 355 | 356 | line.move(to: startPoint) 357 | line.addLine(to: endPoint) 358 | color.setStroke() 359 | line.stroke() 360 | } 361 | 362 | // MARK: Dynamic Animator 363 | 364 | open func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator) { 365 | precondition(Thread.isMainThread) 366 | 367 | // Prevent the deceleration behavior to be called on rotation (overwriting 368 | // the offset set with frame.didSet) 369 | animator.removeAllBehaviors() 370 | 371 | let panRecognizer = gestureRecognizers?[0] as? PanGestureRecognizer 372 | // The picker can become invisible between the moment the user starts to 373 | // swipe accross it and when the deceleration ends. This occurs when 374 | // the user rotates the screen or navigates to the previous/next screen. 375 | let visible = window != nil 376 | 377 | guard let action = action, let target = target, let recognizer = panRecognizer, visible else { 378 | return 379 | } 380 | sendAction(action, to: target, for: recognizer.endEvent) 381 | } 382 | } 383 | 384 | 385 | // MARK: Unit Utilities 386 | 387 | public func shouldUseMetricSystem() -> Bool { 388 | return Locale.current.usesMetricSystem && Locale.current.regionCode != "GB" 389 | } 390 | 391 | public func metersFromMiles(_ miles: Double) -> Double { 392 | if miles == .greatestFiniteMagnitude { 393 | return .greatestFiniteMagnitude 394 | } 395 | return miles * 1609.344 396 | } 397 | 398 | public func milesFromMeters(_ meters: Double) -> Double { 399 | if meters == .greatestFiniteMagnitude { 400 | return .greatestFiniteMagnitude 401 | } 402 | return meters * 0.000621371192 403 | } 404 | 405 | 406 | // MARK: Private Classes 407 | 408 | private class PanGestureRecognizer : UIPanGestureRecognizer { 409 | 410 | var endEvent: UIEvent? 411 | 412 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 413 | super.touchesBegan(touches, with: event) 414 | endEvent = nil 415 | } 416 | 417 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 418 | super.touchesEnded(touches, with: event) 419 | endEvent = event 420 | } 421 | } 422 | 423 | 424 | private class DynamicItem : NSObject, UIDynamicItem { 425 | 426 | @objc var center = CGPoint.zero 427 | // Bounds must be initialized with a size bigger than zero to prevent an exception 428 | @objc var bounds = CGRect(x: 0, y: 0, width: 1, height: 1) 429 | @objc var transform = CGAffineTransform() 430 | } 431 | -------------------------------------------------------------------------------- /DistancePicker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6009E2611D5226B3006424BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2601D5226B3006424BD /* AppDelegate.swift */; }; 11 | 6009E2631D5226B3006424BD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2621D5226B3006424BD /* ViewController.swift */; }; 12 | 6009E2661D5226B3006424BD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6009E2641D5226B3006424BD /* Main.storyboard */; }; 13 | 6009E2681D5226B3006424BD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6009E2671D5226B3006424BD /* Assets.xcassets */; }; 14 | 6009E26B1D5226B3006424BD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6009E2691D5226B3006424BD /* LaunchScreen.storyboard */; }; 15 | 6009E2791D522764006424BD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6009E2781D522764006424BD /* MapKit.framework */; }; 16 | 600E8C4F1D5894FF00AFD10E /* DistancePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = 600E8C4E1D5894FB00AFD10E /* DistancePicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | 6065F4991D54AA320063187A /* DistancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6009E2721D522704006424BD /* DistancePicker.swift */; }; 18 | 6065F49A1D54AB6D0063187A /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6009E2781D522764006424BD /* MapKit.framework */; }; 19 | 6065F49C1D54AB730063187A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6065F49B1D54AB730063187A /* Foundation.framework */; }; 20 | 6065F49E1D54AB7D0063187A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6065F49D1D54AB7D0063187A /* UIKit.framework */; }; 21 | 6065F4A01D54ADDF0063187A /* DistancePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6065F48C1D54A9F30063187A /* DistancePicker.framework */; }; 22 | 6065F4A11D54ADDF0063187A /* DistancePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6065F48C1D54A9F30063187A /* DistancePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 6065F4A21D54ADDF0063187A /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 6009E2551D5226B3006424BD /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 6065F48B1D54A9F30063187A; 31 | remoteInfo = DistancePicker; 32 | }; 33 | /* End PBXContainerItemProxy section */ 34 | 35 | /* Begin PBXCopyFilesBuildPhase section */ 36 | 6065F4981D54A9F30063187A /* Embed Frameworks */ = { 37 | isa = PBXCopyFilesBuildPhase; 38 | buildActionMask = 2147483647; 39 | dstPath = ""; 40 | dstSubfolderSpec = 10; 41 | files = ( 42 | 6065F4A11D54ADDF0063187A /* DistancePicker.framework in Embed Frameworks */, 43 | ); 44 | name = "Embed Frameworks"; 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXCopyFilesBuildPhase section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 6009E25D1D5226B3006424BD /* DistancePickerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DistancePickerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 6009E2601D5226B3006424BD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 52 | 6009E2621D5226B3006424BD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 53 | 6009E2651D5226B3006424BD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 54 | 6009E2671D5226B3006424BD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 55 | 6009E26A1D5226B3006424BD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 56 | 6009E26C1D5226B3006424BD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57 | 6009E2721D522704006424BD /* DistancePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DistancePicker.swift; sourceTree = ""; }; 58 | 6009E2741D522715006424BD /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 59 | 6009E2761D52271C006424BD /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 60 | 6009E2781D522764006424BD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; 61 | 600E8C4E1D5894FB00AFD10E /* DistancePicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DistancePicker.h; sourceTree = SOURCE_ROOT; }; 62 | 6023BE4C1F93D06000D70C5E /* NEWS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = NEWS.md; sourceTree = ""; }; 63 | 6065F48C1D54A9F30063187A /* DistancePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DistancePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 6065F4901D54A9F30063187A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 65 | 6065F49B1D54AB730063187A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 66 | 6065F49D1D54AB7D0063187A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 6009E25A1D5226B3006424BD /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | 6009E2791D522764006424BD /* MapKit.framework in Frameworks */, 75 | 6065F4A01D54ADDF0063187A /* DistancePicker.framework in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | 6065F4881D54A9F30063187A /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | 6065F49E1D54AB7D0063187A /* UIKit.framework in Frameworks */, 84 | 6065F49C1D54AB730063187A /* Foundation.framework in Frameworks */, 85 | 6065F49A1D54AB6D0063187A /* MapKit.framework in Frameworks */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXFrameworksBuildPhase section */ 90 | 91 | /* Begin PBXGroup section */ 92 | 6009E2541D5226B3006424BD = { 93 | isa = PBXGroup; 94 | children = ( 95 | 6009E2741D522715006424BD /* README.md */, 96 | 6023BE4C1F93D06000D70C5E /* NEWS.md */, 97 | 6009E2761D52271C006424BD /* LICENSE */, 98 | 6009E2721D522704006424BD /* DistancePicker.swift */, 99 | 6065F48D1D54A9F30063187A /* Supporting Files */, 100 | 6009E25F1D5226B3006424BD /* Example */, 101 | 6009E2811D523996006424BD /* Frameworks */, 102 | 6009E25E1D5226B3006424BD /* Products */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | 6009E25E1D5226B3006424BD /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 6009E25D1D5226B3006424BD /* DistancePickerExample.app */, 110 | 6065F48C1D54A9F30063187A /* DistancePicker.framework */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | 6009E25F1D5226B3006424BD /* Example */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 6009E2601D5226B3006424BD /* AppDelegate.swift */, 119 | 6009E2621D5226B3006424BD /* ViewController.swift */, 120 | 6009E2641D5226B3006424BD /* Main.storyboard */, 121 | 6009E2671D5226B3006424BD /* Assets.xcassets */, 122 | 6009E2691D5226B3006424BD /* LaunchScreen.storyboard */, 123 | 6009E26C1D5226B3006424BD /* Info.plist */, 124 | ); 125 | path = Example; 126 | sourceTree = ""; 127 | }; 128 | 6009E2811D523996006424BD /* Frameworks */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 6065F49D1D54AB7D0063187A /* UIKit.framework */, 132 | 6065F49B1D54AB730063187A /* Foundation.framework */, 133 | 6009E2781D522764006424BD /* MapKit.framework */, 134 | ); 135 | name = Frameworks; 136 | sourceTree = ""; 137 | }; 138 | 6065F48D1D54A9F30063187A /* Supporting Files */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 600E8C4E1D5894FB00AFD10E /* DistancePicker.h */, 142 | 6065F4901D54A9F30063187A /* Info.plist */, 143 | ); 144 | name = "Supporting Files"; 145 | sourceTree = ""; 146 | }; 147 | /* End PBXGroup section */ 148 | 149 | /* Begin PBXHeadersBuildPhase section */ 150 | 6065F4891D54A9F30063187A /* Headers */ = { 151 | isa = PBXHeadersBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 600E8C4F1D5894FF00AFD10E /* DistancePicker.h in Headers */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXHeadersBuildPhase section */ 159 | 160 | /* Begin PBXNativeTarget section */ 161 | 6009E25C1D5226B3006424BD /* DistancePickerExample */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = 6009E26F1D5226B3006424BD /* Build configuration list for PBXNativeTarget "DistancePickerExample" */; 164 | buildPhases = ( 165 | 6009E2591D5226B3006424BD /* Sources */, 166 | 6009E25A1D5226B3006424BD /* Frameworks */, 167 | 6009E25B1D5226B3006424BD /* Resources */, 168 | 6065F4981D54A9F30063187A /* Embed Frameworks */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | 6065F4A31D54ADDF0063187A /* PBXTargetDependency */, 174 | ); 175 | name = DistancePickerExample; 176 | productName = DistancePicker; 177 | productReference = 6009E25D1D5226B3006424BD /* DistancePickerExample.app */; 178 | productType = "com.apple.product-type.application"; 179 | }; 180 | 6065F48B1D54A9F30063187A /* DistancePicker */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = 6065F4951D54A9F30063187A /* Build configuration list for PBXNativeTarget "DistancePicker" */; 183 | buildPhases = ( 184 | 6065F4871D54A9F30063187A /* Sources */, 185 | 6065F4881D54A9F30063187A /* Frameworks */, 186 | 6065F4891D54A9F30063187A /* Headers */, 187 | 6065F48A1D54A9F30063187A /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | ); 193 | name = DistancePicker; 194 | productName = DistancePicker; 195 | productReference = 6065F48C1D54A9F30063187A /* DistancePicker.framework */; 196 | productType = "com.apple.product-type.framework"; 197 | }; 198 | /* End PBXNativeTarget section */ 199 | 200 | /* Begin PBXProject section */ 201 | 6009E2551D5226B3006424BD /* Project object */ = { 202 | isa = PBXProject; 203 | attributes = { 204 | LastSwiftUpdateCheck = 0730; 205 | LastUpgradeCheck = 1020; 206 | ORGANIZATIONNAME = "Quentin Mathé"; 207 | TargetAttributes = { 208 | 6009E25C1D5226B3006424BD = { 209 | CreatedOnToolsVersion = 7.3; 210 | DevelopmentTeam = WZ4X77MGB4; 211 | LastSwiftMigration = 1020; 212 | }; 213 | 6065F48B1D54A9F30063187A = { 214 | CreatedOnToolsVersion = 7.3; 215 | LastSwiftMigration = 1020; 216 | }; 217 | }; 218 | }; 219 | buildConfigurationList = 6009E2581D5226B3006424BD /* Build configuration list for PBXProject "DistancePicker" */; 220 | compatibilityVersion = "Xcode 3.2"; 221 | developmentRegion = en; 222 | hasScannedForEncodings = 0; 223 | knownRegions = ( 224 | en, 225 | Base, 226 | ); 227 | mainGroup = 6009E2541D5226B3006424BD; 228 | productRefGroup = 6009E25E1D5226B3006424BD /* Products */; 229 | projectDirPath = ""; 230 | projectRoot = ""; 231 | targets = ( 232 | 6009E25C1D5226B3006424BD /* DistancePickerExample */, 233 | 6065F48B1D54A9F30063187A /* DistancePicker */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | 6009E25B1D5226B3006424BD /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | 6009E26B1D5226B3006424BD /* LaunchScreen.storyboard in Resources */, 244 | 6009E2681D5226B3006424BD /* Assets.xcassets in Resources */, 245 | 6009E2661D5226B3006424BD /* Main.storyboard in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | 6065F48A1D54A9F30063187A /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 6009E2591D5226B3006424BD /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 6009E2631D5226B3006424BD /* ViewController.swift in Sources */, 264 | 6009E2611D5226B3006424BD /* AppDelegate.swift in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | 6065F4871D54A9F30063187A /* Sources */ = { 269 | isa = PBXSourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | 6065F4991D54AA320063187A /* DistancePicker.swift in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXTargetDependency section */ 279 | 6065F4A31D54ADDF0063187A /* PBXTargetDependency */ = { 280 | isa = PBXTargetDependency; 281 | target = 6065F48B1D54A9F30063187A /* DistancePicker */; 282 | targetProxy = 6065F4A21D54ADDF0063187A /* PBXContainerItemProxy */; 283 | }; 284 | /* End PBXTargetDependency section */ 285 | 286 | /* Begin PBXVariantGroup section */ 287 | 6009E2641D5226B3006424BD /* Main.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | 6009E2651D5226B3006424BD /* Base */, 291 | ); 292 | name = Main.storyboard; 293 | sourceTree = ""; 294 | }; 295 | 6009E2691D5226B3006424BD /* LaunchScreen.storyboard */ = { 296 | isa = PBXVariantGroup; 297 | children = ( 298 | 6009E26A1D5226B3006424BD /* Base */, 299 | ); 300 | name = LaunchScreen.storyboard; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXVariantGroup section */ 304 | 305 | /* Begin XCBuildConfiguration section */ 306 | 6009E26D1D5226B3006424BD /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ALWAYS_SEARCH_USER_PATHS = NO; 310 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 311 | CLANG_ANALYZER_NONNULL = YES; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 331 | CLANG_WARN_STRICT_PROTOTYPES = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | DEBUG_INFORMATION_FORMAT = dwarf; 338 | ENABLE_STRICT_OBJC_MSGSEND = YES; 339 | ENABLE_TESTABILITY = YES; 340 | GCC_C_LANGUAGE_STANDARD = gnu99; 341 | GCC_DYNAMIC_NO_PIC = NO; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_OPTIMIZATION_LEVEL = 0; 344 | GCC_PREPROCESSOR_DEFINITIONS = ( 345 | "DEBUG=1", 346 | "$(inherited)", 347 | ); 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 355 | ONLY_ACTIVE_ARCH = YES; 356 | SDKROOT = iphoneos; 357 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 358 | SWIFT_VERSION = 4.0; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Debug; 362 | }; 363 | 6009E26E1D5226B3006424BD /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ALWAYS_SEARCH_USER_PATHS = NO; 367 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 368 | CLANG_ANALYZER_NONNULL = YES; 369 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 370 | CLANG_CXX_LIBRARY = "libc++"; 371 | CLANG_ENABLE_MODULES = YES; 372 | CLANG_ENABLE_OBJC_ARC = YES; 373 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 374 | CLANG_WARN_BOOL_CONVERSION = YES; 375 | CLANG_WARN_COMMA = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 378 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 379 | CLANG_WARN_EMPTY_BODY = YES; 380 | CLANG_WARN_ENUM_CONVERSION = YES; 381 | CLANG_WARN_INFINITE_RECURSION = YES; 382 | CLANG_WARN_INT_CONVERSION = YES; 383 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 385 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 386 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 387 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 388 | CLANG_WARN_STRICT_PROTOTYPES = YES; 389 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu99; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 406 | SDKROOT = iphoneos; 407 | SWIFT_COMPILATION_MODE = wholemodule; 408 | SWIFT_VERSION = 4.0; 409 | TARGETED_DEVICE_FAMILY = "1,2"; 410 | VALIDATE_PRODUCT = YES; 411 | }; 412 | name = Release; 413 | }; 414 | 6009E2701D5226B3006424BD /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Brand Assets"; 420 | DEVELOPMENT_TEAM = WZ4X77MGB4; 421 | INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist"; 422 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 423 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DistancePickerExample; 424 | PRODUCT_NAME = "$(TARGET_NAME)"; 425 | SWIFT_VERSION = 5.0; 426 | }; 427 | name = Debug; 428 | }; 429 | 6009E2711D5226B3006424BD /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 433 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 434 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Brand Assets"; 435 | DEVELOPMENT_TEAM = WZ4X77MGB4; 436 | INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist"; 437 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 438 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DistancePickerExample; 439 | PRODUCT_NAME = "$(TARGET_NAME)"; 440 | SWIFT_VERSION = 5.0; 441 | }; 442 | name = Release; 443 | }; 444 | 6065F4961D54A9F30063187A /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | CODE_SIGN_IDENTITY = ""; 448 | DEFINES_MODULE = YES; 449 | DYLIB_COMPATIBILITY_VERSION = 1; 450 | DYLIB_CURRENT_VERSION = 1; 451 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 452 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; 453 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 454 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 455 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DistancePicker; 456 | PRODUCT_NAME = "$(TARGET_NAME)"; 457 | SKIP_INSTALL = YES; 458 | SWIFT_VERSION = 5.0; 459 | }; 460 | name = Debug; 461 | }; 462 | 6065F4971D54A9F30063187A /* Release */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | CODE_SIGN_IDENTITY = ""; 466 | DEFINES_MODULE = YES; 467 | DYLIB_COMPATIBILITY_VERSION = 1; 468 | DYLIB_CURRENT_VERSION = 1; 469 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 470 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; 471 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 472 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 473 | PRODUCT_BUNDLE_IDENTIFIER = com.quentinmathe.DistancePicker; 474 | PRODUCT_NAME = "$(TARGET_NAME)"; 475 | SKIP_INSTALL = YES; 476 | SWIFT_VERSION = 5.0; 477 | }; 478 | name = Release; 479 | }; 480 | /* End XCBuildConfiguration section */ 481 | 482 | /* Begin XCConfigurationList section */ 483 | 6009E2581D5226B3006424BD /* Build configuration list for PBXProject "DistancePicker" */ = { 484 | isa = XCConfigurationList; 485 | buildConfigurations = ( 486 | 6009E26D1D5226B3006424BD /* Debug */, 487 | 6009E26E1D5226B3006424BD /* Release */, 488 | ); 489 | defaultConfigurationIsVisible = 0; 490 | defaultConfigurationName = Release; 491 | }; 492 | 6009E26F1D5226B3006424BD /* Build configuration list for PBXNativeTarget "DistancePickerExample" */ = { 493 | isa = XCConfigurationList; 494 | buildConfigurations = ( 495 | 6009E2701D5226B3006424BD /* Debug */, 496 | 6009E2711D5226B3006424BD /* Release */, 497 | ); 498 | defaultConfigurationIsVisible = 0; 499 | defaultConfigurationName = Release; 500 | }; 501 | 6065F4951D54A9F30063187A /* Build configuration list for PBXNativeTarget "DistancePicker" */ = { 502 | isa = XCConfigurationList; 503 | buildConfigurations = ( 504 | 6065F4961D54A9F30063187A /* Debug */, 505 | 6065F4971D54A9F30063187A /* Release */, 506 | ); 507 | defaultConfigurationIsVisible = 0; 508 | defaultConfigurationName = Release; 509 | }; 510 | /* End XCConfigurationList section */ 511 | }; 512 | rootObject = 6009E2551D5226B3006424BD /* Project object */; 513 | } 514 | -------------------------------------------------------------------------------- /DistancePicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DistancePicker.xcodeproj/xcshareddata/xcschemes/DistancePicker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /DistancePicker.xcodeproj/xcshareddata/xcschemes/DistancePickerExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2016 Quentin Mathe 3 | 4 | Date: August 2016 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | UIApplication.shared.setStatusBarStyle(.lightContent, animated: false) 18 | return true 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Brand Assets.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "ipad", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "1x" 9 | }, 10 | { 11 | "orientation" : "landscape", 12 | "idiom" : "ipad", 13 | "extent" : "full-screen", 14 | "minimum-system-version" : "7.0", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "orientation" : "portrait", 19 | "idiom" : "ipad", 20 | "extent" : "full-screen", 21 | "minimum-system-version" : "7.0", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "orientation" : "landscape", 26 | "idiom" : "ipad", 27 | "extent" : "full-screen", 28 | "minimum-system-version" : "7.0", 29 | "scale" : "2x" 30 | } 31 | ], 32 | "info" : { 33 | "version" : 1, 34 | "author" : "xcode" 35 | } 36 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/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 | -------------------------------------------------------------------------------- /Example/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 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Example/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 | 0.8.4 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 7 23 | LSRequiresIPhoneOS 24 | 25 | NSLocationWhenInUseUsageDescription 26 | If this app example can know your current location, it can show the selected distance as a search radius on the map. 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2016 Quentin Mathe 3 | 4 | Date: August 2016 5 | License: MIT 6 | */ 7 | 8 | import UIKit 9 | import MapKit 10 | import DistancePicker 11 | 12 | class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate { 13 | 14 | @IBOutlet var distancePicker: DistancePicker! 15 | @IBOutlet var mapView: MKMapView! 16 | var searchRadiusOverlay: MKOverlay? 17 | var searchRadiusActive: Bool { 18 | return distancePicker.selectedValue != .greatestFiniteMagnitude && isValidAuthorizationStatus(authorizationStatus) 19 | } 20 | var authorizationStatus = CLAuthorizationStatus.notDetermined 21 | var locationManager = CLLocationManager() 22 | 23 | // MARK: - Configuration 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | // Every time the user manipulates the distance picker, an action is 29 | // sent when the pan animation stops. We use this opportunity to update 30 | // the map rect and search radius to match the selected distance. 31 | distancePicker.target = self 32 | distancePicker.action = #selector(updateUI) 33 | 34 | mapView.delegate = self 35 | mapView.isHidden = true 36 | 37 | locationManager.delegate = self 38 | locationManager.requestWhenInUseAuthorization() 39 | } 40 | 41 | func isValidAuthorizationStatus(_ status: CLAuthorizationStatus) -> Bool { 42 | // For iOS 7, AuthorizedAlways corresponds to Authorized 43 | return status == CLAuthorizationStatus.authorizedWhenInUse || status == CLAuthorizationStatus.authorizedAlways 44 | } 45 | 46 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 47 | authorizationStatus = status 48 | 49 | if isValidAuthorizationStatus(status) { 50 | mapView.showsUserLocation = true 51 | } 52 | updateUI() 53 | } 54 | 55 | // MARK: - Updating UI 56 | 57 | @IBAction func updateUI() { 58 | updateSearchRadiusOverlay() 59 | updateVisibleMapRect() 60 | } 61 | 62 | func updateSearchRadiusOverlay() { 63 | if let overlay = searchRadiusOverlay { 64 | mapView.removeOverlay(overlay) 65 | searchRadiusOverlay = nil 66 | } 67 | 68 | if searchRadiusActive { 69 | searchRadiusOverlay = MKCircle(center: mapView.userLocation.coordinate, 70 | radius: distancePicker.selectedValue) 71 | mapView.addOverlay(searchRadiusOverlay!) 72 | } 73 | } 74 | 75 | func updateVisibleMapRect() { 76 | if searchRadiusActive { 77 | let overlayInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 78 | let mapRect = mapView.mapRectThatFits(searchRadiusOverlay!.boundingMapRect, edgePadding: overlayInset) 79 | 80 | mapView.setVisibleMapRect(mapRect, animated: false) 81 | } 82 | else if isValidAuthorizationStatus(authorizationStatus) { 83 | mapView.setCenter(mapView.userLocation.coordinate, animated: true) 84 | } 85 | // On launch, hide the map until we know the user location, otherwise 86 | // the map is briefly centered on another location. 87 | mapView.isHidden = false 88 | } 89 | 90 | // MARK: - Map View Delegate 91 | 92 | func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { 93 | updateUI() 94 | } 95 | 96 | func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { 97 | if overlay is MKCircle { 98 | 99 | let circle = MKCircleRenderer(overlay: overlay) 100 | 101 | circle.strokeColor = UIColor.red 102 | circle.fillColor = UIColor.red.withAlphaComponent(0.1) 103 | circle.lineWidth = 1 104 | 105 | return circle 106 | } 107 | else { 108 | fatalError("Unexpected overlay type") 109 | } 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /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 | FMWK 17 | CFBundleShortVersionString 18 | 0.8.4 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 7 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Quentin Mathé 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 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | DistancePicker NEWS 2 | ================= 3 | 4 | 0.8.4 5 | ------ 6 | 7 | - Migrated to Swift 5 8 | 9 | 0.8.3 10 | ------ 11 | 12 | - Migrated to Swift 4.2 13 | 14 | 0.8.2 15 | ----- 16 | 17 | - Migrated to Swift 4 18 | 19 | 0.8.1 20 | ----- 21 | 22 | - Migrated to Swift 3 23 | - Fixed missing redraw when rotating the device on iOS 10 or higher 24 | 25 | 0.8 26 | --- 27 | 28 | - First release 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Distance Picker 2 | =============== 3 | 4 | [![Build Status](https://travis-ci.org/qmathe/DistancePicker.svg?branch=master)](https://travis-ci.org/qmathe/DistancePicker) 5 | [![Platforms iOS](https://img.shields.io/badge/Platforms-iOS-lightgray.svg?style=flat)](http://www.apple.com) 6 | [![Language Swift 4](https://img.shields.io/badge/Language-Swift%204.2-orange.svg?style=flat)](https://swift.org) 7 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/qmathe/DistancePicker/LICENSE) 8 | 9 | DistancePicker is a custom UIKit control to select a distance with a pan gesture. It looks like a ruler with multiple distance marks and can be used to resize a map, set up a geofence or choose a search radius. 10 | 11 | Screenshot 12 | 13 | To see in action, take a look at [Placeboard](http://www.placeboardapp.com) demo video. 14 | 15 | Compatibility 16 | ------------- 17 | 18 | DistancePicker requires at least Xcode 9 and supports iOS 8 or higher. 19 | 20 | | Swift | DistancePicker | 21 | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 22 | | 5 | [0.8.4](https://github.com/qmathe/DistancePicker/releases/tag/0.8.4) or master | 23 | | 4.2 | [0.8.3](https://github.com/qmathe/DistancePicker/releases/tag/0.8.3) or branch [swift-4.2](https://github.com/qmathe/DistancePicker/tree/swift-4.2) | 24 | | 4.X | [0.8.2](https://github.com/qmathe/DistancePicker/releases/tag/0.8.2) or branch [swift-4.1](https://github.com/qmathe/DistancePicker/tree/swift-4.1) | 25 | | 3.X | [0.8.1](https://github.com/qmathe/DistancePicker/releases/tag/0.8.1) or branch [swift-3.2](https://github.com/qmathe/DistancePicker/tree/swift-3.2) | 26 | 27 | 28 | Installation 29 | ------------ 30 | 31 | ### Carthage 32 | 33 | Add the following line to your Cartfile, run `carthage update` to build the framework and drag the built DistancePicker.framework into your Xcode project. 34 | 35 | github "qmathe/DistancePicker" 36 | 37 | ### CocoaPods 38 | 39 | Add the following lines to your Podfile and run `pod install` with CocoaPods 0.36 or newer. 40 | 41 | use_frameworks! 42 | 43 | pod "DistancePicker" 44 | 45 | ### Manually 46 | 47 | If you don't use Carthage or CocoaPods, it's possible to drag the built framework or embed the source files into your project. 48 | 49 | #### Framework 50 | 51 | Build DistancePicker framework and drop it into your Xcode project. 52 | 53 | #### Files 54 | 55 | Drop DistancePicker.swift into your Xcode project and link MapKit. 56 | --------------------------------------------------------------------------------