├── .gitignore ├── BlueSwift.podspec ├── Bluetooth.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── BlueSwift-Sample.xcscheme │ └── BlueSwift-iOS.xcscheme ├── Bluetooth ├── AdvertisementViewController.swift ├── AppDelegate.swift ├── ConnectionViewController.swift └── Supporting Files │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── Main.storyboard ├── CHANGELOG.md ├── Configurations ├── Common │ ├── Common-Base.xcconfig │ ├── Common-Development.xcconfig │ └── Common-Release.xcconfig ├── Framework │ └── Framework-Base.xcconfig ├── Sample │ └── Sample-Base.xcconfig ├── Tests │ └── Tests-Base.xcconfig └── xcconfigs │ ├── Common │ ├── Carthage.xcconfig │ ├── Common.xcconfig │ └── Variables.xcconfig │ ├── Configurations │ ├── Debug.xcconfig │ └── Release.xcconfig │ ├── Documentation │ ├── Best-Practices.md │ ├── Carthage.md │ ├── CocoaPods.md │ ├── Code-Signing.md │ ├── Images │ │ ├── usage-assign-project-configurations.gif │ │ ├── usage-build.gif │ │ ├── usage-delete-build-settings.gif │ │ └── usage-update-info-plist.gif │ ├── Tutorial.md │ └── Variables.md │ ├── LICENSE.md │ ├── Platforms │ ├── iOS.xcconfig │ ├── macOS.xcconfig │ ├── tvOS.xcconfig │ └── watchOS.xcconfig │ ├── README.md │ └── Targets │ ├── Application.xcconfig │ ├── Extension.xcconfig │ ├── Framework.xcconfig │ └── Tests.xcconfig ├── Framework ├── Source Files │ ├── Advertisement │ │ ├── AdvertisementService.swift │ │ └── BluetoothAdvertisement.swift │ ├── Command │ │ └── Command.swift │ ├── Connection │ │ ├── BluetoothConnection.swift │ │ └── ConnectionService.swift │ ├── Extensions │ │ ├── Array.swift │ │ ├── CBCharacteristic.swift │ │ ├── CBManager.swift │ │ ├── CBUUID.swift │ │ ├── Data.swift │ │ ├── Int.swift │ │ ├── Sequence.swift │ │ └── String.swift │ ├── Model │ │ ├── AdvertisementData.swift │ │ ├── BluetoothAuthorizationStatus.swift │ │ ├── BluetoothError.swift │ │ ├── Characteristic.swift │ │ ├── Configuration.swift │ │ ├── Peripheral │ │ │ ├── AdvertisablePeripheral.swift │ │ │ ├── ConnectablePeripheral.swift │ │ │ └── Peripheral.swift │ │ └── Service.swift │ └── Utilities │ │ ├── DispatchQueueProtocol.swift │ │ └── SynchronizedArray.swift └── Supporting Files │ └── Info.plist ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── Package.swift ├── Readme.md ├── Unit Tests ├── CommandTests.swift ├── ConfigurationTests.swift ├── ExtensionTests.swift ├── Info.plist ├── Mocks │ └── MockDispatchQueue.swift ├── SequenceExtensionTests.swift └── SynchronizedArrayTests.swift ├── bitrise.yml ├── docs ├── _config.yml ├── advertisementData.md ├── bluetoothAdvertisement.md ├── bluetoothConnection.md ├── characteristic.md ├── command.md ├── configuration.md ├── index.md ├── peripheral.md └── service.md └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .localized 3 | 4 | xcuserdata 5 | profile 6 | build 7 | .build 8 | output 9 | DerivedData 10 | *.mode1v3 11 | *.mode2v3 12 | *.perspectivev3 13 | *.pbxuser 14 | *.xccheckout 15 | *.moved-aside 16 | *.hmap 17 | *.ipa 18 | *.o 19 | *.LinkFileList 20 | *.hmap 21 | *~.nib 22 | *.swp 23 | *.dat 24 | *.dep 25 | *.framework.zip 26 | 27 | .idea 28 | *.iml 29 | 30 | Pods 31 | Carthage 32 | -------------------------------------------------------------------------------- /BlueSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | 3 | spec.name = 'BlueSwift' 4 | spec.version = '1.1.6' 5 | spec.summary = 'Easy and lightweight CoreBluetooth wrapper written in Swift' 6 | spec.homepage = 'https://github.com/netguru/BlueSwift' 7 | 8 | spec.license = { type: 'MIT', file: 'LICENSE.md' } 9 | spec.authors = { 'Jan Posz' => 'jan.posz@netguru.co' } 10 | spec.source = { git: 'https://github.com/netguru/BlueSwift.git', tag: spec.version.to_s } 11 | 12 | spec.source_files = 'Framework/Source Files/**/*.swift' 13 | 14 | spec.requires_arc = true 15 | spec.frameworks = 'Foundation', 'CoreBluetooth' 16 | 17 | spec.swift_version = '5.3' 18 | spec.ios.deployment_target = '11.0' 19 | 20 | end 21 | -------------------------------------------------------------------------------- /Bluetooth.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Bluetooth.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Bluetooth.xcodeproj/xcshareddata/xcschemes/BlueSwift-Sample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Bluetooth.xcodeproj/xcshareddata/xcschemes/BlueSwift-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 77 | 78 | 79 | 80 | 81 | 91 | 92 | 98 | 99 | 100 | 101 | 107 | 108 | 114 | 115 | 116 | 117 | 119 | 120 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Bluetooth/AdvertisementViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import UIKit 7 | import BlueSwift 8 | 9 | class AdvertisementViewController: UIViewController { 10 | 11 | @IBOutlet weak var notifyTextField: UITextField! 12 | @IBOutlet weak var readTextField: UITextField! 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | let advertisement = BluetoothAdvertisement.shared 16 | lazy var characteristic = try! Characteristic(uuid: "1104FD87-820F-438A-B757-7AC2C15C2D56") 17 | lazy var otherCharacteristic = try! Characteristic(uuid: "1204FD87-820F-438A-B757-7AC2C15C2D56") 18 | lazy var service = try! Service(uuid: "1004FD87-820F-438A-B757-7AC2C15C2D56", characteristics: [characteristic, otherCharacteristic]) 19 | lazy var peripheral: Peripheral = { 20 | let configuration = try! Configuration(services: [service], advertisement: "1004FD87-820F-438A-B757-7AC2C15C2D56") 21 | return Peripheral(configuration: configuration, advertisementData: [.localName("Test"), .servicesUUIDs("1004FD87-820F-438A-B757-7AC2C15C2D56")]) 22 | }() 23 | 24 | @IBAction func advertise() { 25 | advertisement.advertise(peripheral: peripheral) { _ in 26 | print("Error during adverisement") 27 | } 28 | handleRequests() 29 | } 30 | 31 | @IBAction func update() { 32 | let command = Command.utf8String(notifyTextField.text!) 33 | advertisement.update(command, characteristic: characteristic) { error in 34 | print("Notified on central.") 35 | } 36 | } 37 | 38 | func handleRequests() { 39 | advertisement.writeRequestCallback = { [weak self] characteristic, data in 40 | guard 41 | var text = self?.textView.text, 42 | let data = data, 43 | let encoded = String(data: data, encoding: .utf8) 44 | else { 45 | return 46 | } 47 | text = text + "\nWrote: \(encoded). Characteristic: \(characteristic.uuid)" 48 | self?.textView.text = text 49 | } 50 | 51 | advertisement.readRequestCallback = { [weak self] characteristic -> Data in 52 | guard 53 | var text = self?.textView.text, 54 | let notifyValue = self?.readTextField.text 55 | else { 56 | return Data() 57 | } 58 | text = text + "\nReceived read request. Characteristic: \(characteristic.uuid)" 59 | self?.textView.text = text 60 | return notifyValue.data(using: .utf8, allowLossyConversion: false)! 61 | } 62 | } 63 | 64 | override func viewWillDisappear(_ animated: Bool) { 65 | super.viewWillDisappear(animated) 66 | advertisement.stopAdvertising() 67 | } 68 | 69 | @IBAction func hideKeyboard() { 70 | view.endEditing(true) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Bluetooth/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | window = UIWindow(frame: UIScreen.main.bounds) 15 | window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() 16 | window?.makeKeyAndVisible() 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Bluetooth/ConnectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import UIKit 7 | import BlueSwift 8 | 9 | class ConnectionViewController: UIViewController { 10 | 11 | @IBOutlet weak var notifyLabel: UILabel! 12 | @IBOutlet weak var readLabel: UILabel! 13 | @IBOutlet weak var textField: UITextField! 14 | 15 | private var loadingState: LoadingState = .connected { 16 | didSet { 17 | let activityIndicator: UIActivityIndicatorView 18 | if #available(iOS 13, *) { 19 | activityIndicator = UIActivityIndicatorView(style: .medium) 20 | } else { 21 | activityIndicator = UIActivityIndicatorView(style: .gray) 22 | } 23 | activityIndicator.startAnimating() 24 | 25 | switch self.loadingState { 26 | case .loading: 27 | navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityIndicator) 28 | title = "Connecting" 29 | case .connected: 30 | navigationItem.rightBarButtonItem = nil 31 | title = "Connected" 32 | case .error(_): 33 | navigationItem.rightBarButtonItem = nil 34 | title = "Connection error" 35 | } 36 | } 37 | } 38 | 39 | let connection = BluetoothConnection.shared 40 | lazy var characteristic = try! Characteristic(uuid: "1104FD87-820F-438A-B757-7AC2C15C2D56", shouldObserveNotification: true) 41 | lazy var otherCharacteristic = try! Characteristic(uuid: "1204FD87-820F-438A-B757-7AC2C15C2D56", shouldObserveNotification: true) 42 | lazy var service = try! Service(uuid: "1004FD87-820F-438A-B757-7AC2C15C2D56", characteristics: [characteristic, otherCharacteristic]) 43 | lazy var peripheral: Peripheral = { 44 | let configuration = try! Configuration(services: [service], advertisement: "1004FD87-820F-438A-B757-7AC2C15C2D56") 45 | return Peripheral(configuration: configuration) 46 | }() 47 | 48 | @IBAction func connect() { 49 | loadingState = .loading 50 | connection.connect(peripheral) { [weak self] error in 51 | if let error = error { 52 | self?.loadingState = .error(error) 53 | self?.title = "Connection failure" 54 | return 55 | } else { 56 | self?.title = "Connected" 57 | self?.loadingState = .connected 58 | } 59 | } 60 | handleNotifications() 61 | } 62 | 63 | func handleNotifications() { 64 | characteristic.notifyHandler = { [weak self] data in 65 | guard 66 | let data = data, 67 | let encoded = String(data: data, encoding: .utf8) 68 | else { 69 | return 70 | } 71 | self?.notifyLabel.text = "Notify result: \(encoded)" 72 | } 73 | } 74 | 75 | @IBAction func write() { 76 | let command = Command.utf8String(textField.text!) 77 | peripheral.write(command: command, characteristic: otherCharacteristic) { error in 78 | print("Did write") 79 | } 80 | } 81 | 82 | @IBAction func read() { 83 | peripheral.read(characteristic, handler: { [weak self] data, error in 84 | guard 85 | let data = data, 86 | let encoded = String(data: data, encoding: .utf8) 87 | else { 88 | return 89 | } 90 | self?.readLabel.text = "Read result: \(encoded)" 91 | }) 92 | } 93 | 94 | override func viewWillDisappear(_ animated: Bool) { 95 | super.viewWillDisappear(animated) 96 | connection.disconnect(peripheral) 97 | } 98 | 99 | @IBAction func hideKeyboard() { 100 | view.endEditing(true) 101 | } 102 | } 103 | 104 | internal extension ConnectionViewController { 105 | 106 | /// Enum representing current state of connecting to the peripheral. 107 | enum LoadingState { 108 | case loading 109 | case connected 110 | case error(Error) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Bluetooth/Supporting Files/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 | } -------------------------------------------------------------------------------- /Bluetooth/Supporting Files/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Bluetooth/Supporting Files/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 | $(PRODUCT_BUNDLE_VERSION_STRING) 19 | CFBundleVersion 20 | $(PRODUCT_BUNDLE_VERSION) 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 | -------------------------------------------------------------------------------- /Bluetooth/Supporting Files/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 | 45 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 86 | 87 | 88 | 89 | 90 | 91 | 98 | 99 | 100 | 101 | 102 | 103 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 169 | 170 | 171 | 172 | 173 | 174 | 181 | 188 | 194 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [1.1.6] - 2023-10-27 5 | 6 | ### Changed 7 | 8 | - Fix Swift 5.9 (Xcode 15) build error caused by `Data(bytes:count:)` initializer. 9 | 10 | ## [1.1.5] - 2023-01-24 11 | 12 | ### Changed 13 | 14 | - Fix official cocoapods release 15 | 16 | ## [1.1.4] - 2023-01-20 17 | 18 | ### Changed 19 | 20 | - Synchronize access to array storing peripherals to increase thread safety. 21 | 22 | 23 | ## [1.1.3] - 2022-10-25 24 | 25 | ### Changed 26 | 27 | - Peripheral which was connected, but it was not on the list of peripherals that should be connected, is disconnected. This change allows to discover this peripheral again. 28 | 29 | ## [1.1.2] - 2022-09-06 30 | 31 | ### Added 32 | 33 | - added settable `centralManagerStateUpdateHandler` public property to `BluetoothConnection` to monitor updates to Central Manager state. 34 | 35 | ## [1.1.1] - 2022-04-14 36 | 37 | ### Added 38 | 39 | - `bluetoothAuthorizationStatus` public property was added to `BluetoothConnection` to determine current Bluetooth authorization status. 40 | - `requestBluetoothAuthorization()` public method was added to `BluetoothConnection` to requests User for authorization to use Bluetooth. 41 | - `BluetoothAuthorizationStatus` enum describing Bluetooth authorization status 42 | 43 | ### Changed 44 | 45 | - `ConnectionService`: centralManager (`CBCentralManager`) is instantiated lazily to postpone showing Bluetooth authorization popup until it is needed. 46 | 47 | ## [1.1.0] - 2022-04-13 48 | 49 | ### Changed 50 | 51 | - enabled Swift library evolution (`BUILD_LIBRARY_FOR_DISTRIBUTION = YES`) to allow creating XCFrameworks using BlueSwift. See ["Library Evolution in Swift"](https://www.swift.org/blog/library-evolution/) official swift blogpost. 52 | This change: 53 | - allows modules built with different compiler versions to be used together in one app. 54 | - allows developers of binary frameworks to make additive changes to the API of their framework while remaining binary compatible with previous versions. 55 | 56 | ## [1.0.6] - 2022-04-08 57 | 58 | ### Added 59 | 60 | - added public `peripheralConnectionCancelledHandler(_:)` setable property to `BluetoothConnection` class. It is called when disconnecting a peripheral using `disconnect(_:)` is completed 61 | 62 | ### Changed 63 | 64 | - refactored `.filter(_:).first` to `first(where:)` for optimisation 65 | -------------------------------------------------------------------------------- /Configurations/Common/Common-Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | #include "../xcconfigs/Common/Common.xcconfig" 7 | 8 | _BUILD_VERSION = 1.1.6 9 | _BUILD_NUMBER = 1 10 | 11 | _DEPLOYMENT_TARGET_IOS = 11.0 12 | _COMPILER_SWIFT_VERSION = 5.3.0 13 | -------------------------------------------------------------------------------- /Configurations/Common/Common-Development.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | #include "Common-Base.xcconfig" 7 | #include "../xcconfigs/Configurations/Debug.xcconfig" 8 | -------------------------------------------------------------------------------- /Configurations/Common/Common-Release.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | #include "Common-Base.xcconfig" 7 | #include "../xcconfigs/Configurations/Release.xcconfig" 8 | -------------------------------------------------------------------------------- /Configurations/Framework/Framework-Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | #include "../xcconfigs/Platforms/iOS.xcconfig" 7 | #include "../xcconfigs/Targets/Framework.xcconfig" 8 | 9 | _BUNDLE_NAME = BlueSwift 10 | _BUNDLE_IDENTIFIER = co.netguru.lib.blueswift 11 | _BUNDLE_INFOPLIST_PATH = $(PROJECT_DIR)/Framework/Supporting Files/Info.plist 12 | 13 | ENABLE_TESTABILITY = YES 14 | 15 | // Build library for distribution to enable Swift library evolution 16 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES 17 | -------------------------------------------------------------------------------- /Configurations/Sample/Sample-Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | #include "../xcconfigs/Platforms/iOS.xcconfig" 7 | #include "../xcconfigs/Targets/Application.xcconfig" 8 | 9 | _BUNDLE_NAME = BlueSwiftSample 10 | _BUNDLE_IDENTIFIER = co.netguru.lib.blueswift.sample 11 | _BUNDLE_INFOPLIST_PATH = $(PROJECT_DIR)/Bluetooth/Supporting Files/Info.plist 12 | 13 | _CODESIGN_STYLE = Manual 14 | _CODESIGN_IDENTITY = iPhone Developer 15 | -------------------------------------------------------------------------------- /Configurations/Tests/Tests-Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | #include "../xcconfigs/Platforms/iOS.xcconfig" 7 | #include "../xcconfigs/Targets/Tests.xcconfig" 8 | 9 | _BUNDLE_NAME = BlueSwiftTests 10 | _BUNDLE_IDENTIFIER = co.netguru.lib.blueswift.tests 11 | _BUNDLE_INFOPLIST_PATH = $(PROJECT_DIR)/Unit Tests/Info.plist 12 | 13 | TEST_HOST = $(BUILT_PRODUCTS_DIR)/BlueSwiftSample.app/BlueSwiftSample 14 | BUNDLE_LOADER = $(TEST_HOST) 15 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Common/Carthage.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Common/Carthage.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains base build settings for targets using Carthage. 8 | // 9 | 10 | // Path to top-level Carthage directory. 11 | _CARTHAGE_PATH = $(PROJECT_DIR)/Carthage 12 | 13 | // Path to platform-specific Carthage/Build directory. 14 | _CARTHAGE_BUILD_PATH[sdk=iphone*] = $(_CARTHAGE_PATH)/Build/iOS 15 | _CARTHAGE_BUILD_PATH[sdk=macosx*] = $(_CARTHAGE_PATH)/Build/Mac 16 | _CARTHAGE_BUILD_PATH[sdk=appletv*] = $(_CARTHAGE_PATH)/Build/tvOS 17 | _CARTHAGE_BUILD_PATH[sdk=watch*] = $(_CARTHAGE_PATH)/Build/watchOS 18 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Common/Common.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Common/Common.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains base build settings for all targets. 8 | // 9 | 10 | // Include default values of variables. 11 | #include "Variables.xcconfig" 12 | 13 | // MARK: Build options 14 | 15 | // The format of debugging symbols. 16 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 17 | 18 | // MARK: Search paths 19 | 20 | // Disable legacy-compatible header searching. 21 | ALWAYS_SEARCH_USER_PATHS = NO 22 | 23 | // Additional framework search paths. 24 | FRAMEWORK_SEARCH_PATHS = $(inherited) $(_CARTHAGE_BUILD_PATH) $(_COMPILER_FRAMEWORK_SEARCH_PATHS) 25 | 26 | // Additional header search paths. 27 | HEADER_SEARCH_PATHS = $(inherited) $(_COMPILER_OBJC_HEADER_SEARCH_PATHS) 28 | 29 | // MARK: Linking 30 | 31 | // Other linker flags. 32 | OTHER_LDFLAGS = $(inherited) $(_COMPILER_OBJC_LINKER_FLAGS) 33 | 34 | // MARK: Packaging 35 | 36 | // Path to `Info.plist` file. 37 | INFOPLIST_FILE = $(_BUNDLE_INFOPLIST_PATH) 38 | 39 | // `CFBundleVersionString`. 40 | PRODUCT_BUNDLE_VERSION_STRING = $(_BUILD_VERSION) 41 | 42 | // `CFBundleVersion`. 43 | PRODUCT_BUNDLE_VERSION = $(_BUILD_NUMBER) 44 | 45 | // `CFBundleIdentifier`. 46 | PRODUCT_BUNDLE_IDENTIFIER = $(_BUNDLE_IDENTIFIER) 47 | 48 | // Name of the product. 49 | PRODUCT_NAME = $(_BUNDLE_NAME) 50 | 51 | // MARK: Signing 52 | 53 | // Code signing style. 54 | CODE_SIGN_STYLE = $(_CODESIGN_STYLE) 55 | 56 | // Development team to use for code signing. 57 | DEVELOPMENT_TEAM = $(_CODESIGN_DEVELOPMENT_TEAM) 58 | 59 | // Identity to use for code signing. 60 | CODE_SIGN_IDENTITY = $(_CODESIGN_IDENTITY) 61 | 62 | // Provisioning profile specifier for code signing. 63 | PROVISIONING_PROFILE_SPECIFIER = $(_CODESIGN_PROFILE_SPECIFIER) 64 | 65 | // Path to `.entitlements` file for code signing. 66 | CODE_SIGN_ENTITLEMENTS = $(_CODESIGN_ENTITLEMENTS_PATH) 67 | 68 | // MARK: LLVM compiler 69 | 70 | // Disable link-time optimizations. 71 | LLVM_LTO = NO 72 | 73 | // Compile `NSAssert` assertions. 74 | ENABLE_NS_ASSERTIONS = YES 75 | 76 | // Require `objc_msgSend` to be cast before invocation. 77 | ENABLE_STRICT_OBJC_MSGSEND = YES 78 | 79 | // Preprocessor definitions, mapped from `_ENVIRONMENT`. 80 | GCC_PREPROCESSOR_DEFINITIONS = $(_ENVIRONMENTS) $(inherited) 81 | 82 | // Use GNU++11 C++ language standard. 83 | CLANG_CXX_LANGUAGE_STANDARD = gnu++11 84 | 85 | // Use LLVM C++ standard library. 86 | CLANG_CXX_LIBRARY = libc++ 87 | 88 | // Enable module imports and definitions. 89 | CLANG_ENABLE_MODULES = YES 90 | 91 | // Enable Objective-C ARC. 92 | CLANG_ENABLE_OBJC_ARC = YES 93 | 94 | // Warn about block capture autoreleasing. 95 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 96 | 97 | // Warn about suspucious commas. 98 | CLANG_WARN_COMMA = YES 99 | 100 | // Warn about suspicious empty loop bodies. 101 | CLANG_WARN_EMPTY_BODY = YES 102 | 103 | // Warn when a function will recursively call itself on every code path. 104 | CLANG_WARN_INFINITE_RECURSION = YES 105 | 106 | // Warn about non-literal null conversion. 107 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 108 | 109 | // Whether to warn on suspicious implicit conversions. 110 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES 111 | 112 | // Warn about strict prototypes. 113 | CLANG_WARN_STRICT_PROTOTYPES = YES 114 | 115 | // Warn about unreachable code. 116 | CLANG_WARN_UNREACHABLE_CODE = YES 117 | 118 | // Warn about range-based `for` loops in C++. 119 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 120 | 121 | // Warning about suspicious uses of `std::move`. 122 | CLANG_WARN_SUSPICIOUS_MOVE = YES 123 | 124 | // Warn when overriding deprecated Objective-C methods. 125 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 126 | 127 | // Warn about implicit Objective-C literal conversions. 128 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES 129 | 130 | // Warn about direct accesses to the Objective-C `isa` pointer instead of using 131 | // the Objective-C runtime API. 132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 133 | 134 | // Warn about classes that unintentionally do not subclass a root class. 135 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 136 | 137 | // Warn duplicate method declarations within the same `@interface`. 138 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 139 | 140 | // Warn about implicit capture of `self` (e.g. direct ivar access). 141 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 142 | 143 | // Whether to run the static analyzer with every build. 144 | RUN_CLANG_STATIC_ANALYZER = YES 145 | 146 | // Warn for misused nullability attributes. 147 | CLANG_ANALYZER_NONNULL = YES 148 | 149 | // Warn when a non-localized string is passed to a user-interface method 150 | // expecting a localized string. 151 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES 152 | 153 | // Warn about suspicius conversions between `NSNumber` and `CFNumberRef`. 154 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE 155 | 156 | // Warn when a floating-point value is used as a loop counter. 157 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES 158 | 159 | // Warn when using `rand()` and `random()` instead of `arc4random()`. 160 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES 161 | 162 | // Warn when using `strcpy() and `strcat()`. 163 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES 164 | 165 | // Warn about incorrect uses of `nullable` values. 166 | CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES 167 | 168 | // Function calls should not be position-dependent. 169 | GCC_DYNAMIC_NO_PIC = NO 170 | 171 | // Warn if the same variable is declared in two binaries that are linked 172 | // together. 173 | GCC_NO_COMMON_BLOCKS = YES 174 | 175 | // Use GNU99 C variant. 176 | GCC_C_LANGUAGE_STANDARD = gnu99 177 | 178 | // Whether to precompile the prefix header (if one is specified). 179 | GCC_PRECOMPILE_PREFIX_HEADER = YES 180 | 181 | // Treat warnings as errors (a.k.a. hard mode). 182 | GCC_TREAT_WARNINGS_AS_ERRORS = $(_COMPILER_HARD_MODE) 183 | 184 | // Warn about 64-bit values being implicitly shortened to 32 bits. 185 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 186 | 187 | // Warn about fields missing from structure initializers (only if designated 188 | // initializers aren't used). 189 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES 190 | 191 | // Warn when the returned value does not match its return type. 192 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 193 | 194 | // Warn about the use of four-character constants. 195 | GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES 196 | 197 | // Warn about an aggregate data type's initializer not being fully bracketed. 198 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES 199 | 200 | // Whether to warn about unsafe comparisons between values of different 201 | // signedness. 202 | GCC_WARN_SIGN_COMPARE = YES 203 | 204 | // Warn if a variable might be clobbered by a setjmp call or if an automatic 205 | // variable is used without prior initialization. 206 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 207 | 208 | // Warn about static functions that are unused. 209 | GCC_WARN_UNUSED_FUNCTION = YES 210 | 211 | // Whether to warn about labels that are unused. 212 | GCC_WARN_UNUSED_LABEL = YES 213 | 214 | // Warn about variables that are never used. 215 | GCC_WARN_UNUSED_VARIABLE = YES 216 | 217 | // Warn if a `@selector` expression refers to an undeclared selector. 218 | GCC_WARN_UNDECLARED_SELECTOR = YES 219 | 220 | // MARK: Asset catalog compiler 221 | 222 | // Asset to use as app icon. 223 | ASSETCATALOG_COMPILER_APPICON_NAME = $(_ASSET_ICON) 224 | 225 | // Asset to use as launch image. 226 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = $(_ASSET_LAUNCHIMAGE) 227 | 228 | // MARK: Swift compiler 229 | 230 | // Swift version to be used. 231 | SWIFT_VERSION = $(_COMPILER_SWIFT_VERSION) 232 | 233 | // Treat warning as errors (a.k.a. hard mode). 234 | SWIFT_TREAT_WARNINGS_AS_ERRORS = $(_COMPILER_HARD_MODE) 235 | 236 | // Compilation conditions for Swift, mapped from `_ENVIRONMENTS`. 237 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(_ENVIRONMENTS) $(inherited) 238 | 239 | // Additional flags passed to Swift compiler. 240 | OTHER_SWIFT_FLAGS = $(inherited) $(_COMPILER_SWIFT_FLAGS) 241 | 242 | // Path to bridging header between Swift and Objective-C. 243 | SWIFT_OBJC_BRIDGING_HEADER = $(_COMPILER_SWIFT_BRIDGING_HEADER_PATH) 244 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Common/Variables.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Common/Variables.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains variables that are mapped to build settings. 8 | // 9 | 10 | // MARK: Environment 11 | 12 | // The environments build setting, typically overridden in environment-specific 13 | // `xcconfig` files. 14 | _ENVIRONMENTS = ENV_DEFAULT 15 | 16 | // MARK: Versioning 17 | 18 | // The build semantic version, mapped later to `CFBundleVersionString`. 19 | _BUILD_VERSION = 20 | 21 | // The build number, mapped later to `CFBundleVersion`. 22 | _BUILD_NUMBER = $(_BUILD_VERSION) 23 | 24 | // MARK: Bundle 25 | 26 | // Bundle name of the product, used by Xcode to create an `.app` bundle. 27 | _BUNDLE_NAME = $(TARGET_NAME) 28 | 29 | // Bundle identifier of the product. 30 | _BUNDLE_IDENTIFIER = 31 | 32 | /// Path to `Info.plist` file of the product. 33 | _BUNDLE_INFOPLIST_PATH = 34 | 35 | // Whether the bundle is `@testable`. 36 | _BUNDLE_TESTABLE = YES 37 | 38 | // MARK: Deployment 39 | 40 | // Minimum iOS deployment target. 41 | _DEPLOYMENT_TARGET_IOS = 42 | 43 | // Targeted device families on iOS. 44 | _DEPLOYMENT_DEVICES_IOS = 1,2 45 | 46 | // Minimum macOS deployment target. 47 | _DEPLOYMENT_TARGET_MACOS = 48 | 49 | // Minimum tvOS deployment target. 50 | _DEPLOYMENT_TARGET_TVOS = 51 | 52 | // Targeted device families on tvOS. 53 | _DEPLOYMENT_DEVICES_TVOS = 3 54 | 55 | // Minimum watchOS deployment target. 56 | _DEPLOYMENT_TARGET_WATCHOS = 57 | 58 | // Targeted device families on watchOS. 59 | _DEPLOYMENT_DEVICES_WATCHOS = 4 60 | 61 | // MARK: Signing 62 | 63 | // Code signign style. 64 | _CODESIGN_STYLE = Automatic 65 | 66 | // Development team to be used with `_CODESIGN_PROFILE_SPECIFIER` to manually 67 | // code sign the product. 68 | _CODESIGN_DEVELOPMENT_TEAM = 69 | 70 | // The identity to be used with `_CODESIGN_PROFILE_SPECIFIER` to manually code 71 | // sign the product. 72 | _CODESIGN_IDENTITY = 73 | 74 | // The provisioning profile specifier to be used with `_CODESIGN_IDENTITY` to 75 | // manually code sign the product. 76 | _CODESIGN_PROFILE_SPECIFIER = 77 | 78 | // Path to `.entitlements` file of the product. 79 | _CODESIGN_ENTITLEMENTS_PATH = 80 | 81 | // MARK: Assets 82 | 83 | // Name of the icon asset to be used. 84 | _ASSET_ICON = 85 | 86 | // Name of the launch image asset to be used. 87 | _ASSET_LAUNCHIMAGE = 88 | 89 | // MARK: Compiler 90 | 91 | // Whether to enable hard mode (a.k.a. treat warnings as errors). 92 | _COMPILER_HARD_MODE = YES 93 | 94 | // The version of Swift to be used by compiler. 95 | _COMPILER_SWIFT_VERSION = 96 | 97 | // Additional flags for the Swift compiler. 98 | _COMPILER_SWIFT_FLAGS = 99 | 100 | // Path to Swift bridging header to be used by compiler. 101 | _COMPILER_SWIFT_BRIDGING_HEADER_PATH = 102 | 103 | // Additional flags for Objective-C linker. 104 | _COMPILER_OBJC_LINKER_FLAGS = 105 | 106 | // Additional search paths when looking for headers. 107 | _COMPILER_OBJC_HEADER_SEARCH_PATHS = 108 | 109 | // Additional search paths when looking for frameworks. 110 | _COMPILER_FRAMEWORK_SEARCH_PATHS = 111 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Configurations/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Configurations/Debug.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to debug configuration. 8 | // 9 | 10 | // MARK: Environment 11 | 12 | // Set debug environments. 13 | _ENVIRONMENTS = ENV_DEBUG 14 | 15 | // MARK: Architecture 16 | 17 | // Build only the active architecture. 18 | ONLY_ACTIVE_ARCH = YES 19 | 20 | // MARK: Build options 21 | 22 | // Allow `@testable` imports. 23 | ENABLE_TESTABILITY = $(_BUNDLE_TESTABLE) 24 | 25 | // MARK: Deployment 26 | 27 | // Do not strip debugging symbols when copying resources. 28 | COPY_PHASE_STRIP = NO 29 | 30 | // Do not strip debugging symbols when copying the built product to its final 31 | // installation location. 32 | STRIP_INSTALLED_PRODUCT = NO 33 | 34 | // MARK: Signing 35 | 36 | // Disable Developer ID timestamping. 37 | OTHER_CODE_SIGN_FLAGS = --timestamp=none 38 | 39 | // MARK: LLVM compiler 40 | 41 | // Disable GCC optimization. 42 | GCC_OPTIMIZATION_LEVEL = 0 43 | 44 | // Catch errors in integer arithmetic. 45 | OTHER_CFLAGS = -ftrapv 46 | 47 | // MARK: Asset compiler 48 | 49 | // Optimize assets for time. 50 | ASSETCATALOG_COMPILER_OPTIMIZATION = time 51 | 52 | // MARK: Swift compiler 53 | 54 | // Disable optimizations for Swift. 55 | SWIFT_OPTIMIZATION_LEVEL = -Onone 56 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Configurations/Release.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Configurations/Release.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to release configuration. 8 | // 9 | 10 | // MARK: Environment 11 | 12 | // Set release environments. 13 | _ENVIRONMENTS = ENV_RELEASE 14 | 15 | // MARK: Architecture 16 | 17 | // Build for all architectures. 18 | ONLY_ACTIVE_ARCH = NO 19 | 20 | // MARK: Build options 21 | 22 | // Disallow `@testable` imports. 23 | ENABLE_TESTABILITY = NO 24 | 25 | // MARK: Deployment 26 | 27 | // Strip debugging symbols when copying resources. 28 | COPY_PHASE_STRIP = YES 29 | 30 | // Strip debugging symbols when copying the built product to its final 31 | // installation location. 32 | STRIP_INSTALLED_PRODUCT = YES 33 | 34 | // MARK: LLVM compiler 35 | 36 | // Enable GCC optimization. 37 | GCC_OPTIMIZATION_LEVEL = s 38 | 39 | // MARK: Asset compiler 40 | 41 | // Optimize assets for space. 42 | ASSETCATALOG_COMPILER_OPTIMIZATION = space 43 | 44 | // MARK: Swift compiler 45 | 46 | // Enable whole-module-optimization for Swift. 47 | SWIFT_OPTIMIZATION_LEVEL = -Owholemodule 48 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Best-Practices.md: -------------------------------------------------------------------------------- 1 | # xcconfigs – Best Practices 2 | 3 | Each project has best practices on how to do various things, and [netguru/xcconfigs](https://github.com/netguru/xcconfigs) is no exception. 4 | 5 | --- 6 | 7 | ### Prefer creating custom build setting variables instead of new xcconfig files 8 | 9 | In case you need more control over which build setting is used in which target and configuration, instead of creating multiple `.xcconfig` files with similar, but slightly various content, prefer to create custom variables and map those to the real build settings instead. 10 | 11 | For example, in case you need to use different Bundle Identifier for multiple build configurations, prefer the following setup: 12 | 13 | ```none 14 | // Application.xcconfig 15 | 16 | _BUNDLE_IDENTIFIER = com.example.app$(__BUNDLE_IDENTIFIER_SUFFIX) 17 | ``` 18 | 19 | ```none 20 | // Development.xcconfig 21 | 22 | __BUNDLE_IDENTIFIER_SUFFIX = .development 23 | ``` 24 | 25 | ```none 26 | // Staging.xcconfig 27 | 28 | __BUNDLE_IDENTIFIER_SUFFIX = .staging 29 | ``` 30 | 31 | Instead of this setup: 32 | 33 | ```none 34 | // Application-Development.xcconfig 35 | 36 | _BUNDLE_IDENTIFIER = com.example.app.development 37 | ``` 38 | 39 | ```none 40 | // Application-Staging.xcconfig 41 | 42 | _BUNDLE_IDENTIFIER = com.example.app.staging 43 | ``` 44 | 45 | ```none 46 | // Application-Release.xcconfig 47 | 48 | _BUNDLE_IDENTIFIER = com.example.app 49 | ``` 50 | 51 | _Rationale: Keeping `.xcconfig` files simple and using custom build setting variables makes your configuration more maintainable and easier to understand for newcomers._ 52 | 53 | --- 54 | 55 | ### Prefix your custom build setting variables by double underscore 56 | 57 | If you use custom build setting variables, prefix them by double underscore `__`. 58 | 59 | For example, **prefer this name**: 60 | 61 | ```none 62 | __FOO = 63 | ``` 64 | 65 | Instead of these names: 66 | 67 | ```none 68 | _BAR = 69 | BAZ = 70 | ``` 71 | 72 | _Rationale: Build settings with no underscores are "real build settings" used by compiler and understood by Xcode. Build settings with one underscore are used by [netguru/xcconfigs](https://github.com/netguru/xcconfigs). By using double underscore, you minimize the risk of accidentally overriding build settings from any of aforementioned places._ 73 | 74 | --- 75 | 76 | ### Use variables provided by [netguru/xcconfigs](https://github.com/netguru/xcconfigs) instead of real build settings 77 | 78 | In case you need to customize build settings, prefer to override [variables](Variables.md) provided by [netguru/xcconfigs](https://github.com/netguru/xcconfigs). 79 | 80 | Additionally, try to inherit defult build settings by putting `$(inherited)` as a value of build setting that support inheritance. 81 | 82 | In other words, **prefer this**: 83 | 84 | ```none 85 | _COMPILER_FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)/Custom/Path/To/Frameworks 86 | ``` 87 | 88 | Instead of this: 89 | 90 | ```none 91 | FRAMEWORK_SEARCH_PATHS = $(PROJECT_DIR)/Custom/Path/To/Frameworks 92 | ``` 93 | 94 | _Rationale: [netguru/xcconfigs](https://github.com/netguru/xcconfigs) uses some reasonable default values for many build settings and some of them are even composed out of many variables._ 95 | 96 | --- 97 | 98 | ### Don't fight Xcode in case of problems 99 | 100 | [netguru/xcconfigs](https://github.com/netguru/xcconfigs) are here to help you, not interfere with or disable your work. 101 | 102 | If Xcode throws strange errors during compilation (or runtime), you suspect it's caused by using this project and you don't know how to fix it, let Xcode win (by using build settings in `*.xcodeproj` file or getting rid of `*.xcconfig` files at all). 103 | 104 | In addition, in case of problems, you are encouraged to [open an issue](https://github.com/netguru/xcconfigs/issues/new) so that our maintainers can help you. 105 | 106 | _Rationale: You don't want to fight your IDE._ 107 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Carthage.md: -------------------------------------------------------------------------------- 1 | # xcconfigs - Carthage 2 | 3 | This document describes best practices of working with [Carthage](https://github.com/Carthage/Carthage) using [netguru/xcconfigs](https://github.com/netguru/xcconfigs). 4 | 5 | --- 6 | 7 | ### Step 1: Include Carthage.xcconfig file 8 | 9 | This example assumes that a base `Base.xcconfig` file exists that is imported (explicitly or implicitly) by all other `*.xcconfig` files. 10 | 11 | In your `Base.xcconfig`, include `Carthage.xcconfig` file: 12 | 13 | ```none 14 | // Base.xcconfig 15 | 16 | #import "path/to/xcconfigs/Common/Carthge.xcconfig" 17 | ``` 18 | 19 | By importing this file, you now have access to `_CARTHAGE_BUILD_PATH` build setting variable that is automatically used in `FRAMEWORK_SEARCH_PATHS` and that you can use in `carthage copy-frameworks` build phase. 20 | 21 | Note that by default [netguru/xcconfigs](https://github.com/netguru/xcconfigs) assumes that `Carthage` directory is in `$(PROJECT_DIR)/Carthage`. If you need to customize that, override `_CARTHAGE_PATH` in your `Base.xcconfig`: 22 | 23 | ```none 24 | // Base.xcconfig 25 | 26 | #include "path/to/xcconfigs/Common/Carthge.xcconfig" 27 | _CARTHAGE_PATH = $(PROJECT_DIR)/Custom/Path/To/Carthage 28 | ``` 29 | 30 | ### Step 2: Use \_CARTHAGE_BUILD_PATH in copy-frameworks build phase 31 | 32 | When providing framework paths as inputs to your `carthage copy-frameworks` build phase, instead of providing explicit paths, you can take advantage of aforementioned `_CARTHAGE_BUILD_PATH` build setting variable. 33 | 34 | As a result, your input file list will look like this, **no matter the platform**: 35 | 36 | ```none 37 | $(_CARTHAGE_BUILD_PATH)/Result.framework 38 | $(_CARTHAGE_BUILD_PATH)/ReactiveSwift.framework 39 | $(_CARTHAGE_BUILD_PATH)/ReactiveCocoa.framework 40 | ``` 41 | 42 | Instead of like this: 43 | 44 | ```none 45 | Carthage/Build/iOS/Result.framework 46 | Carthage/Build/iOS/ReactiveSwift.framework 47 | Carthage/Build/iOS/ReactiveCocoa.framework 48 | ``` 49 | 50 | This will allow you to copy-paste the list in case you have a multi-platform project with multiple targets depending on the same frameworks, thus making the whole setup more maintainable and less error-prone. 51 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/CocoaPods.md: -------------------------------------------------------------------------------- 1 | # xcconfigs - CocoaPods 2 | 3 | This document describes best practices of working with [CocoaPods](https://cocoapods.org) using [netguru/xcconfigs](https://github.com/netguru/xcconfigs). 4 | 5 | --- 6 | 7 | ### Step 1: Provide proper configuration for CocoaPods 8 | 9 | For this example, let's assume you want to create an iOS app target with unit tests and have three build configurations in your project – `Development`, `Staging` and `Release`. 10 | 11 | To be sure that CocoaPods takes this into consideration (and doesn't assume you use default `Debug` and `Release` ones), provide information about them in your `Podfile`: 12 | 13 | ```none 14 | project 'MyProject', 15 | 'Development' => :debug, 16 | 'Staging' => :release, 17 | 'Release' => :release 18 | 19 | target 'Application' do 20 | ... 21 | end 22 | 23 | target 'Tests' do 24 | ... 25 | end 26 | ``` 27 | 28 | Based on that, CocoaPods will generate the following `*.xcconfig` hierarchy: 29 | 30 | ```none 31 | Pods/Target Support Files 32 | │ 33 | ├ Pods-Application 34 | │ ├ Pods-Application.development.xcconfig 35 | │ ├ Pods-Application.staging.xcconfig 36 | │ ├ Pods-Application.release.xcconfig 37 | │ └ ... 38 | │ 39 | ├ Pods-Tests 40 | │ ├ Pods-Tests.development.xcconfig 41 | │ ├ Pods-Tests.staging.xcconfig 42 | │ ├ Pods-Tests.release.xcconfig 43 | │ └ ... 44 | │ 45 | └ ... 46 | ``` 47 | 48 | Yes, all of those should be included in appropriate places in your `*.xcconfig` files... 49 | 50 | ### Step 2: Recreate your own \*.xcconfig files 51 | 52 | As `#include` statements cannot be conditional in `*.xcconfig` files and as CocoaPods will complain if you ignore to include them, you have to other option than to recreate the above configuration: 53 | 54 | ```none 55 | Development - Development.xcconfig 56 | ├ Application - Application-Development.xcconfig 57 | └ Tests - Tests-Development.xcconfig 58 | 59 | Staging - Staging.xcconfig 60 | ├ Application - Application-Staging.xcconfig 61 | └ Tests - Tests-Staging.xcconfig 62 | 63 | Release - Release.xcconfig 64 | ├ Application - Application-Release.xcconfig 65 | └ Tests - Tests-Release.xcconfig 66 | ``` 67 | 68 | It's good to create additional `Common.xcconfig`, `Application-Common.xcconfig` and `Tests-Common.xcconfig` files so that settings can be shared between all the versions. The next step assumes you did that. 69 | 70 | ### Step 3: Include CocoaPods's \*.xcconfig files in your ones 71 | 72 | Now, add proper `#include`s in your `*.xcconfig` files: 73 | 74 | ```none 75 | // Application-Common.xcconfig 76 | 77 | #include "path/to/xcconfigs/Platforms/iOS.xcconfig" 78 | #include "path/to/xcconfigs/Targets/Application.xcconfig" 79 | ``` 80 | 81 | ```none 82 | // Application-Development.xcconfig 83 | 84 | #include "Application-Common.xcconfig" 85 | #include "path/to/Pods/Target Support Files/Pods-Application/Pods-Application.development.xcconfig" 86 | ``` 87 | 88 | ```none 89 | // Application-Staging.xcconfig 90 | 91 | #include "Application-Common.xcconfig" 92 | #include "path/to/Pods/Target Support Files/Pods-Application/Pods-Application.staging.xcconfig" 93 | ``` 94 | 95 | ```none 96 | // Application-Release.xcconfig 97 | 98 | #include "Application-Common.xcconfig" 99 | #include "path/to/Pods/Target Support Files/Pods-Application/Pods-Application.release.xcconfig" 100 | ``` 101 | 102 | ```none 103 | // Tests-Common.xcconfig 104 | 105 | #include "path/to/xcconfigs/Platforms/iOS.xcconfig" 106 | #include "path/to/xcconfigs/Targets/Tests.xcconfig" 107 | ``` 108 | 109 | ```none 110 | // Tests-Development.xcconfig 111 | 112 | #include "Tests-Common.xcconfig" 113 | #include "path/to/Pods/Target Support Files/Pods-Application/Pods-Tests.development.xcconfig" 114 | ``` 115 | 116 | ```none 117 | // Tests-Staging.xcconfig 118 | 119 | #include "Tests-Common.xcconfig" 120 | #include "path/to/Pods/Target Support Files/Pods-Application/Pods-Tests.staging.xcconfig" 121 | ``` 122 | 123 | ```none 124 | // Tests-Release.xcconfig 125 | 126 | #include "Tests-Common.xcconfig" 127 | #include "path/to/Pods/Target Support Files/Pods-Application/Pods-Tests.release.xcconfig" 128 | ``` 129 | 130 | ### Step 4: Build the project! 131 | 132 | Everything should be configured properly by now. Test the configuration by building the project. If it builds without errors, all is good! 133 | 134 | If not, sorry... :trollface: [Please open an issue](https://github.com/netguru/xcconfigs/issues/new). 135 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Code-Signing.md: -------------------------------------------------------------------------------- 1 | # xcconfigs – Code Signing 2 | 3 | Large-scale apps, especially the ones crated by a team of numerous people and having more than two (debug/release) build configurations, need powerful and flexible way of specifying code signing settings per each target and configuration. 4 | 5 | In this document you will see a couple of methods to do flexible manual code signing using [netguru/xcconfigs](https://github.com/netguru/xcconfigs). 6 | 7 | --- 8 | 9 | ### Method 1: Using custom build setting variables 10 | 11 | The first method is to map `_CODESIGN_*` build settings to custom build setting variables declared in configuration `.xcconfig` files. 12 | 13 | The following example assumes that `.xcconfig` files are assigned as follows: 14 | 15 | ```none 16 | Development - Development.xcconfig 17 | ├ Application - Application.xcconfig 18 | └ Tests - Tests.xcconfig 19 | 20 | Staging - Staging.xcconfig 21 | ├ Application - Application.xcconfig 22 | └ Tests - Tests.xcconfig 23 | 24 | Release - Release.xcconfig 25 | ├ Application - Application.xcconfig 26 | └ Tests - Tests.xcconfig 27 | ``` 28 | 29 | In `Application.xcconfig` and `Tests.xcconfig` map `_CODESIGN_*` build settings to custom build settings variables that we will use in the next step: 30 | 31 | ```none 32 | // Application.xcconfig 33 | 34 | _CODESIGN_STYLE = Manual 35 | _CODESIGN_DEVELOPMENT_TEAM = $(__APPLICATION_CODESIGN_DEVELOPMENT_TEAM) 36 | _CODESIGN_IDENTITY = $(__APPLICATION_CODESIGN_IDENTITY) 37 | _CODESIGN_PROFILE_SPECIFIER = $(__APPLICATION_CODESIGN_PROFILE_SPECIFIER) 38 | ``` 39 | 40 | Then, in `Development.xcconfig`, `Staging.xcconfig` and `Release.xcconfig` set values of previously mentioned custom build setting variables: 41 | 42 | ```none 43 | // Staging.xcconfig 44 | 45 | __APPLICATION_CODESIGN_DEVELOPMENT_TEAM = A1B2C3D4E5F6 46 | __APPLICATION_CODESIGN_IDENTITY = John Doe (A1B2C3D4E5F6) 47 | __APPLICATION_CODESIGN_PROFILE_SPECIFIER = MyProject Staging 48 | ``` 49 | 50 | ```none 51 | // Release.xcconfig 52 | 53 | __APPLICATION_CODESIGN_DEVELOPMENT_TEAM = F6E5D4C3B2A1 54 | __APPLICATION_CODESIGN_IDENTITY = Jane Roe (F6E5D4C3B2A1) 55 | __APPLICATION_CODESIGN_PROFILE_SPECIFIER = MyProject Release 56 | ``` 57 | 58 | There are two things worth noting about this example. 59 | 60 | Firstly, notice that `Development.xcconfig` does not set `__APPLICATION_CODESIGN_*` custom build setting variables because you'd probably don't want your debug builds to be code signed. 61 | 62 | Secondly, notice that unit tests bundle target remains not code signed, as `Tests.xcconfig` does not use `__APPLICATION_CODESIGN_*` custom build setting variables. 63 | 64 | If you want your unit tests bundle to be code signed (or you have another target that needs to be code signed), you can use the above method to define more custom build setting variables accordingly (such as `__TESTS_CODESIGN_*` in `Tests.xcconfig`). 65 | 66 | --- 67 | 68 | ### Method 2: Using multiple xcconfig files 69 | 70 | The second method is to use multiple `.xcconfig` files, one per target per build configuration. 71 | 72 | The following example assumes that `.xcconfig` files are assigned as follows: 73 | 74 | ```none 75 | Development - Development.xcconfig 76 | ├ Application - Application-Development.xcconfig 77 | └ Tests - Tests-Development.xcconfig 78 | 79 | Staging - Staging.xcconfig 80 | ├ Application - Application-Staging.xcconfig 81 | └ Tests - Tests-Staging.xcconfig 82 | 83 | Release - Release.xcconfig 84 | ├ Application - Application-Release.xcconfig 85 | └ Tests - Tests-Release.xcconfig 86 | ``` 87 | 88 | In such case, you can directly set `_CODESIGN_*` build settings in `Application-Staging.xcconfig` and `Application-Release.xcconfig` files: 89 | 90 | ```none 91 | // Application-Staging.xcconfig 92 | 93 | _CODESIGN_STYLE = Manual 94 | _CODESIGN_DEVELOPMENT_TEAM = A1B2C3D4E5F6 95 | _CODESIGN_IDENTITY = John Doe (A1B2C3D4E5F6) 96 | _CODESIGN_PROFILE_SPECIFIER = MyProject Staging 97 | ``` 98 | 99 | ```none 100 | // Application-Release.xcconfig 101 | 102 | _CODESIGN_STYLE = Manual 103 | _CODESIGN_DEVELOPMENT_TEAM = F6E5D4C3B2A1 104 | _CODESIGN_IDENTITY = Jane Roe (F6E5D4C3B2A1) 105 | _CODESIGN_PROFILE_SPECIFIER = MyProject Release 106 | ``` 107 | 108 | Like after the first method, `Application-Development.xcconfig` does not set `_CODESIGN_*` build settings and, as neither do `Tests-*.xcconfig` files, unit tests bundle remains not code signed. 109 | 110 | Using this method, as opposed to the first method, does get rid of custom build setting variables. However, it requires 9, instead of 5 `.xcconfig` files to be created. It is up to you and your team mates to decide which method suits you better. 111 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Images/usage-assign-project-configurations.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/BlueSwift/dacfbd87b80ca2593ab2c402c551eea3c8a56821/Configurations/xcconfigs/Documentation/Images/usage-assign-project-configurations.gif -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Images/usage-build.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/BlueSwift/dacfbd87b80ca2593ab2c402c551eea3c8a56821/Configurations/xcconfigs/Documentation/Images/usage-build.gif -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Images/usage-delete-build-settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/BlueSwift/dacfbd87b80ca2593ab2c402c551eea3c8a56821/Configurations/xcconfigs/Documentation/Images/usage-delete-build-settings.gif -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Images/usage-update-info-plist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/BlueSwift/dacfbd87b80ca2593ab2c402c551eea3c8a56821/Configurations/xcconfigs/Documentation/Images/usage-update-info-plist.gif -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Tutorial.md: -------------------------------------------------------------------------------- 1 | # xcconfigs – Tutorial 2 | 3 | This document contains a tutorial on how to set up a new project using [netguru/xcconfigs](https://github.com/netguru/xcconfigs). 4 | 5 | --- 6 | 7 | ### Step 1: Install [netguru/xcconfigs](https://github.com/netguru/xcconfigs) 8 | 9 | Start by installing [netguru/xcconfigs](https://github.com/netguru/xcconfigs). In this example we'll use Carthage. 10 | 11 | Add the following dependency to your `Cartfile`: 12 | 13 | ```none 14 | github "netguru/xcconfigs" {version} 15 | ``` 16 | 17 | Then, while in the root directory of your project, update the dependencies by running: 18 | 19 | ```sh 20 | $ carthage update xcconfigs 21 | ``` 22 | 23 | This will add [netguru/xcconfigs](https://github.com/netguru/xcconfigs) to `Carthage/Checkouts/xcconfigs` directory. 24 | 25 | ### Step 2: Remove build settings from \*.xcodeproj file 26 | 27 | Open your project and remove default build settings created by Xcode. 28 | 29 | To do this, select the project in Navigator panel, go to Build Settings, select all by pressing `⌘A` and then nuke everything by pressing `⌫`. Repeat this step for all targets. 30 | 31 | ![](Images/usage-delete-build-settings.gif) 32 | 33 | ### Step 3: Create your custom \*.xcconfig files 34 | 35 | In order to use [netguru/xcconfigs](https://github.com/netguru/xcconfigs), you need to create your own `*.xcconfig` files and `#include` appropriate [netguru/xcconfigs](https://github.com/netguru/xcconfigs) files in them. 36 | 37 | In this example, we'll create configuration files for a sample iOS app and its test bundle and we'll use the following structure: 38 | 39 | | File | Purpose | Includes | 40 | |:--------------|:------------------------------------------------------------|:---------------------------------------------------------------| 41 | | `Base` | Contains base build settings used by all targets. | `xcconfigs/Common/Common` | 42 | | `Debug` | Contains base build settings used in Debug configuration | `Base`
`xcconfigs/Configurations/Debug` | 43 | | `Release` | Contains base build settings used in Release configuration. | `Base`
`xcconfigs/Configurations/Release` | 44 | | `Application` | Contains build settings used by Application target. | `xcconfigs/Platforms/iOS`
`xcconfigs/Targets/Application` | 45 | | `Tests` | Contains build settings used by Unit Tests target. | `xcconfigs/Platforms/iOS`
`xcconfigs/Targets/Tests` | 46 | 47 | After this step, you should have the following directory tree: 48 | 49 | ```none 50 | Root 51 | │ 52 | ├ Carthage 53 | │ └ Checkouts 54 | │ └ xcconfigs 55 | │ └ ... 56 | │ 57 | ├ Configuration 58 | │ ├ Base.xcconfig 59 | │ ├ Debug.xcconfig 60 | │ ├ Release.xcconfig 61 | │ ├ Application.xcconfig 62 | │ └ Tests.xcconfig 63 | │ 64 | ├ YourProject.xcodeproj 65 | │ └ ... 66 | │ 67 | └ ... 68 | ``` 69 | 70 | Note that before you actually fill your own `*.xcconfig` files, the project **will not build**, as it's still missing the required build settings. 71 | 72 | ### Step 4: Fill your custom \*.xcconfig files 73 | 74 | Now it's time to add appropriate `#include`s and build settings in your custom `*.xcconfig` files. 75 | 76 | ```none 77 | // Base.xcconfig 78 | 79 | #include "../Carthage/Checkouts/xcconfigs/Common/Common.xcconfig" 80 | 81 | _BUILD_VERSION = 1.1 82 | _DEPLOYMENT_TARGET_IOS = 8.0 83 | ``` 84 | 85 | ```none 86 | // Debug.xcconfig 87 | 88 | #include "Base.xcconfig" 89 | #include "../Carthage/Checkouts/xcconfigs/Configurations/Debug.xcconfig" 90 | ``` 91 | 92 | ```none 93 | // Release.xcconfig 94 | 95 | #include "Base.xcconfig" 96 | #include "../Carthage/Checkouts/xcconfigs/Configurations/Release.xcconfig" 97 | ``` 98 | 99 | ```none 100 | // Application.xcconfig 101 | 102 | #include "../Carthage/Checkouts/xcconfigs/Platforms/iOS.xcconfig" 103 | #include "../Carthage/Checkouts/xcconfigs/Targets/Application.xcconfig" 104 | 105 | _BUNDLE_IDENTIFIER = com.example.foo 106 | _BUNDLE_INFOPLIST_PATH = $(PROJECT_DIR)/Path/To/Application-Info.plist 107 | ``` 108 | 109 | ```none 110 | // Tests.xcconfig 111 | 112 | #include "../Carthage/Checkouts/xcconfigs/Platforms/iOS.xcconfig" 113 | #include "../Carthage/Checkouts/xcconfigs/Targets/Tests.xcconfig" 114 | 115 | _BUNDLE_IDENTIFIER = com.example.foo.tests 116 | _BUNDLE_INFOPLIST_PATH = $(PROJECT_DIR)/Path/To/Tests-Info.plist 117 | ``` 118 | 119 | For more information on build setting variables, see [Variables.md](Variables.md). 120 | 121 | ### Step 5: Update your Info.plist file 122 | 123 | Now, in order to manage your build version, build number, bundle name and bundle identifier in your `Info.plist` files, you need to update them accordingly. 124 | 125 | To do that, open both your app's and your unit tests bundle's `Info.plist` and change the following keys: 126 | 127 | | Key | New Value | 128 | |:------------------------------------------------------------------|:-----------------------------------| 129 | | `CFBundleName`
(Bundle name) | `$(PRODUCT_NAME)` | 130 | | `CFBundleIdentifier`
(Bundle identifier) | `$(PRODUCT_BUNDLE_IDENTIFIER)` | 131 | | `CFBundleVersion`
(Bundle version) | `$(PRODUCT_BUNDLE_VERSION)` | 132 | | `CFBundleShortVersionString`
(Bundle versions string, short) | `$(PRODUCT_BUNDLE_VERSION_STRING)` | 133 | 134 | After the change, the `Info.plist` should look like in the below screenshot: 135 | 136 | ![](Images/usage-update-info-plist.gif) 137 | 138 | ### Step 6: Assign configurations in \*.xcodeproj file 139 | 140 | This is the final step. 141 | 142 | Select the project file in Navigator panel, go to project's General tab and assign appropriate `*.xcconfig` files in the Configurations section: 143 | 144 | ![](Images/usage-assign-project-configurations.gif) 145 | 146 | If Xcode can't see your custom `*.xcconfig` files, make sure they are included in the project. 147 | 148 | ### Step 7: Build the project! 149 | 150 | Everything should be configured properly by now. Test the configuration by building the project. If it builds without errors, all is good! 151 | 152 | ![](Images/usage-build.gif) 153 | 154 | If not, sorry... :trollface: [Please open an issue](https://github.com/netguru/xcconfigs/issues/new). 155 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Documentation/Variables.md: -------------------------------------------------------------------------------- 1 | # xcconfigs - Variables 2 | 3 | [netguru/xcconfigs](https://github.com/netguru/xcconfigs) takes advantage of build setting variables to allow more flexible project configuration in comparison to using raw build settings. 4 | 5 | This document describes all public variables that you can override in your own `.xcconfig` files. 6 | 7 | --- 8 | 9 | ### Environment 10 | 11 | #### `_ENVIRONMENTS` 12 | 13 | Environments to be used by compiler. As this variable is mapped to compilation conditions, you may use it to write conditionally compiled code. 14 | 15 | Assuming you have a follofing `*.xcconfig` file: 16 | 17 | ```none 18 | // Staging.xcconfig 19 | 20 | _ENVIRONMENTS = ENV_STAGING 21 | ``` 22 | 23 | You can use this in both Swift and Objective-C code: 24 | 25 | ```swift 26 | // Swift 27 | 28 | #if ENV_STAGING 29 | ... 30 | #endif 31 | ``` 32 | 33 | ```objc 34 | // Objective-C 35 | 36 | #if defined(ENV_STAGING) 37 | ... 38 | #endif 39 | ``` 40 | 41 | You may have more than one environment set up. 42 | 43 | --- 44 | 45 | ### Versioning 46 | 47 | #### `_BUILD_VERSION` 48 | 49 | Semantic version of the product or the whole project. 50 | 51 | #### `_BUILD_NUMBER` 52 | 53 | Build number of the product or the whole project. 54 | 55 | This build setting is particularly useful on a CI service. For example, assuming you use [Travis CI](https://travis-ci.com), you can directly set `_BUILD_NUMBER=$TRAVIS_BUILD_NUMBER` build setting when invoking `xcodebuild`. 56 | 57 | --- 58 | 59 | ### Bundle 60 | 61 | #### `_BUNDLE_NAME` 62 | 63 | Name of the product bundle. 64 | 65 | This is different from Target Name, in that it is used as a name of the executable. This means it has more restrictions when it comes to allowed characters – for example, Framework targets might be called `MyFramework (iOS)`, but, as `_BUNDLE_NAME` doesn't allow spaces, it has to be changed to just `MyFramework` or `MyFramework_iOS`. 66 | 67 | #### `_BUNDLE_IDENTIFIER` 68 | 69 | Identifier of the product bundle. 70 | 71 | This variable is used as `CFBundleIdentifier` in `Info.plist`. 72 | 73 | #### `_BUNDLE_INFOPLIST_PATH` 74 | 75 | Path to `Info.plist` file of the product bundle. 76 | 77 | #### `_BUNDLE_TESTABLE` 78 | 79 | Whether the product bundle is `@testable`. 80 | 81 | --- 82 | 83 | ### Deployment 84 | 85 | #### `_DEPLOYMENT_TARGET_IOS` 86 | 87 | Minimum deployment target of the product on iOS. 88 | 89 | #### `_DEPLOYMENT_DEVICES_IOS` 90 | 91 | Targeted device families on iOS. `1` means iPhone and `2` means iPad. 92 | 93 | #### `_DEPLOYMENT_TARGET_MACOS` 94 | 95 | Minimum deployment target of the product on macOS. 96 | 97 | #### `_DEPLOYMENT_TARGET_TVOS` 98 | 99 | Minimum deployment target of the product on tvOS. 100 | 101 | #### `_DEPLOYMENT_DEVICES_TVOS` 102 | 103 | Targeted device families on tvOS. `3` means Apple TV and is the only valid value. 104 | 105 | #### `_DEPLOYMENT_TARGET_WATCHOS` 106 | 107 | Minimum deployment target of the product on watchOS. 108 | 109 | #### `_DEPLOYMENT_DEVICES_WATCHOS` 110 | 111 | Targeted device families on watchOS. `4` means Apple Watch and is the only valid value. 112 | 113 | --- 114 | 115 | ### Code signing 116 | 117 | #### `_CODESIGN_STYLE` 118 | 119 | Code signing style. Can be either `Automatic` or `Manual`, case-sensitive. 120 | 121 | #### `_CODESIGN_DEVELOPMENT_TEAM` 122 | 123 | Development team used along with `_CODESIGN_IDENTITY` and `_CODESIGN_PROFILE_SPECIFIER` to manually code sign the product. 124 | 125 | #### `_CODESIGN_IDENTITY` 126 | 127 | Identity used along with `_CODESIGN_DEVELOPMENT_TEAM` and `_CODESIGN_PROFILE_SPECIFIER` to manually code sign the product. 128 | 129 | #### `_CODESIGN_PROFILE_SPECIFIER` 130 | 131 | Provisioning profile specifier used along with `_CODESIGN_DEVELOPMENT_TEAM` and `_CODESIGN_IDENTITY` to manually code sign the product. 132 | 133 | #### `_CODESIGN_ENTITLEMENTS_PATH` 134 | 135 | Path to `*.entitlements` file containing sandboxed capabilities of the product. 136 | 137 | --- 138 | 139 | ### Assets 140 | 141 | #### `_ASSET_ICON` 142 | 143 | Name of an asset that will be used as product application's icon. 144 | 145 | #### `_ASSET_LAUNCHIMAGE` 146 | 147 | Name of an asset that will be used as product application's launch image. 148 | 149 | --- 150 | 151 | ### Compiler 152 | 153 | #### `_COMPILER_HARD_MODE` 154 | 155 | Whether to enable hard mode in Objective-C and Swift compilers. :trollface: 156 | 157 | Hard mode enables "Treat warnings as errors" for both Objective-C and Swift. It should be on by default, however, you could opt-out of it in case you're dealing with a legacy project with lots of low-severity warnings. 158 | 159 | #### `_COMPILER_SWIFT_VERSION` 160 | 161 | Swift language version used by the product. 162 | 163 | #### `_COMPILER_SWIFT_FLAGS` 164 | 165 | Additional flags passed to Swift compiler. 166 | 167 | Of of the most useful flags you can take advantage in debug configurations is `-Xfrontend -debug-time-function-bodies` which will print how much time Swift compiler spent on compiling particular function (or computed property). This is very, very useful for debugging long compile times. 168 | 169 | #### `_COMPILER_SWIFT_BRIDGING_HEADER_PATH` 170 | 171 | Path to Objective-C → Swift bridging header. 172 | 173 | #### `_COMPILER_FRAMEWORK_SEARCH_PATHS` 174 | 175 | Framework search paths of Objective-C and Swift compilers. 176 | 177 | #### `_COMPILER_OBJC_HEADER_SEARCH_PATHS` 178 | 179 | Header search paths of Objective-C compiler. 180 | 181 | #### `_COMPILER_OBJC_LINKER_FLAGS` 182 | 183 | Additional flags passed to Objective-C linker. 184 | 185 | You may use this variable to link your target to libraries, such as `-lxml2`. 186 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2017 Netguru Sp. z o.o. 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 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Platforms/iOS.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Platforms/iOS.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to iOS platform. 8 | // 9 | 10 | // MARK: Architecture 11 | 12 | // SDK root of iOS. 13 | SDKROOT = iphoneos 14 | 15 | // Supported platforms for iOS. 16 | SUPPORTED_PLATFORMS = iphoneos iphonesimulator 17 | 18 | // MARK: Deployment 19 | 20 | // Deployment target for iOS. 21 | IPHONEOS_DEPLOYMENT_TARGET = $(_DEPLOYMENT_TARGET_IOS) 22 | 23 | // Supported device families for iOS. 24 | TARGETED_DEVICE_FAMILY = $(_DEPLOYMENT_DEVICES_IOS) 25 | 26 | // MARK: Linking 27 | 28 | // Where to find embedded frameworks for iOS. 29 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 30 | 31 | // MARK: Search paths 32 | 33 | // Xcode needs this to find archived headers if `SKIP_INSTALL` is set. 34 | HEADER_SEARCH_PATHS = $(inherited) $(OBJROOT)/UninstalledProducts/include 35 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Platforms/macOS.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Platforms/macOS.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to macOS platform. 8 | // 9 | 10 | // MARK: Architecture 11 | 12 | // SDK root of macOS. 13 | SDKROOT = macosx 14 | 15 | // Supported platforms for macOS. 16 | SUPPORTED_PLATFORMS = macosx 17 | 18 | // MARK: Deployment 19 | 20 | // Deployment target for macOS. 21 | MACOSX_DEPLOYMENT_TARGET = $(_DEPLOYMENT_TARGET_MACOS) 22 | 23 | // MARK: Compiler 24 | 25 | // Whether to combine multiple image resolutions into a TIFF. 26 | COMBINE_HIDPI_IMAGES = YES 27 | 28 | // MARK: Linker 29 | 30 | // Where to find embedded frameworks for macOS. 31 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks 32 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Platforms/tvOS.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Platforms/tvOS.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to tvOS platform. 8 | // 9 | 10 | // MARK: Architecture 11 | 12 | // SDK root of tvOS. 13 | SDKROOT = appletvos 14 | 15 | // Supported platforms for tvOS. 16 | SUPPORTED_PLATFORMS = appletvos appletvsimulator 17 | 18 | // MARK: Deployment 19 | 20 | // Deployment target for tvOS. 21 | TVOS_DEPLOYMENT_TARGET = $(_DEPLOYMENT_TARGET_TVOS) 22 | 23 | // Supported device families for tvOS. 24 | TARGETED_DEVICE_FAMILY = $(_DEPLOYMENT_DEVICES_TVOS) 25 | 26 | // MARK: Linker 27 | 28 | // Where to find embedded frameworks for tvOS. 29 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 30 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Platforms/watchOS.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Platforms/watchOS.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to watchOS platform. 8 | // 9 | 10 | // MARK: Architecture 11 | 12 | // SDK root of tvOS. 13 | SDKROOT = watchos 14 | 15 | // Supported platforms for watchOS. 16 | SUPPORTED_PLATFORMS = watchos watchsimulator 17 | 18 | // MARK: Deployment 19 | 20 | // Deployment target for watchOS. 21 | WATCHOS_DEPLOYMENT_TARGET = $(_DEPLOYMENT_TARGET_WATCHOS) 22 | 23 | // Supported device families for watchOS. 24 | TARGETED_DEVICE_FAMILY = $(_DEPLOYMENT_DEVICES_WATCHOS) 25 | 26 | // MARK: Linker 27 | 28 | // Where to find embedded frameworks for watchOS. 29 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 30 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/README.md: -------------------------------------------------------------------------------- 1 | # xcconfigs 2 | 3 | ![](https://img.shields.io/badge/xcode-10.0-green.svg) 4 | [![](https://img.shields.io/github/release/netguru/xcconfigs.svg)](https://github.com/netguru/xcconfigs/releases) 5 | [![](https://img.shields.io/github/license/netguru/xcconfigs.svg)](LICENSE.md) 6 | 7 | This is a collection of common Xcode build configuration files. 8 | 9 | ## Requirements 10 | 11 | The latest stable version of `xcconfigs` is compatible with **Xcode 10.0**. 12 | 13 | ## Documentation 14 | 15 | Documentation, tutorials and how-to's are available in [Documentation](Documentation) folder. 16 | 17 | ## Installation 18 | 19 | ### Carthage 20 | 21 | The preferred way of installing `xcconfigs` is using [Carthage](https://github.com/Carthage/Carthage): 22 | 23 | ```none 24 | github "netguru/xcconfigs" {version} 25 | ``` 26 | 27 | ### Submodule 28 | 29 | You may also install `xcconfigs` as a git submodule: 30 | 31 | ```bash 32 | git submodule add https://github.com/netguru/xcconfigs.git {path} 33 | git submodule update --init --recursive 34 | ``` 35 | 36 | ## Roadmap 37 | 38 | #### Version 0.7 39 | 40 | The main focus of this release will be fixing issues encountered when integrating version 0.6. 41 | 42 | ## About 43 | 44 | This project is made and maintained with ♡ by [Netguru](https://netguru.co). 45 | 46 | ### License 47 | 48 | This project is licensed under **MIT License**. See [LICENSE.md](LICENSE.md) for more info. 49 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Targets/Application.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Targets/Application.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to application targets. 8 | // 9 | 10 | // MARK: Packaging 11 | 12 | // Define LLVM module. 13 | DEFINES_MODULE = YES 14 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Targets/Extension.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Targets/Extension.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to extension targets. 8 | // 9 | 10 | // Inherit from Application target. 11 | #include "Application.xcconfig" 12 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Targets/Framework.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Targets/Framework.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to framework targets. 8 | // 9 | 10 | // MARK: Build options 11 | 12 | // Disallow use of APIs that are not available to app extensions and linking to 13 | // frameworks that have not been built with this setting enabled. 14 | APPLICATION_EXTENSION_API_ONLY = YES 15 | 16 | // MARK: Deployment 17 | 18 | // Install in run path. 19 | INSTALL_PATH = @rpath 20 | 21 | // Skip installation. 22 | SKIP_INSTALL = YES 23 | 24 | // MARK: Linking 25 | 26 | // Enable framework to be included from any location as long as the run path 27 | // includes it. 28 | LD_DYLIB_INSTALL_NAME = @rpath/$(PRODUCT_NAME).$(WRAPPER_EXTENSION)/$(PRODUCT_NAME) 29 | 30 | // MARK: Packaging 31 | 32 | // Define LLVM module. 33 | DEFINES_MODULE = YES 34 | -------------------------------------------------------------------------------- /Configurations/xcconfigs/Targets/Tests.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Targets/Tests.xcconfig 3 | // 4 | // Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 5 | // Licensed under MIT License. 6 | // 7 | // This file contains build settings specific to test bundle targets. 8 | // 9 | 10 | // Inherit from Application target. 11 | #include "Application.xcconfig" 12 | -------------------------------------------------------------------------------- /Framework/Source Files/Advertisement/AdvertisementService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// Class implementing peripheral manager delegate. Manages advertisement state. 10 | internal final class AdvertisementService: NSObject { 11 | 12 | /// Peripheral manager used for advertisement. 13 | /// - SeeAlso: `CBPeripheralManager` 14 | private lazy var peripheralManager = CBPeripheralManager(delegate: self, queue: nil) 15 | 16 | /// Boolean value indicating whether device should advertise. 17 | /// Used in Bluetooth state change handler to prevent unwanted advertisement. 18 | private var shouldAdvertise = false 19 | 20 | /// Currently advertising peripheral. 21 | private var peripheral: Peripheral? 22 | 23 | /// After notify subscribtion, subscribed centrals are stored here. 24 | private var subsribedCentrals = [CBCentral]() 25 | 26 | /// Callback called after receiving read request. 27 | internal var readCallback: ((Characteristic) -> (Data))? 28 | 29 | /// Callback called after receiving write request. 30 | internal var writeCallback: ((Characteristic, Data?) -> ())? 31 | 32 | /// Callback called upon upcoming errors. 33 | private var errorHandler: ((AdvertisementError) -> ())? 34 | 35 | /// Starts advertising peripheral with given configuration of services and characteristics. 36 | internal func startAdvertising(_ peripheral: Peripheral, errorHandler: ((AdvertisementError) -> ())?) { 37 | self.peripheral = peripheral 38 | self.errorHandler = errorHandler 39 | peripheralManager.startAdvertising(peripheral.advertisementData?.combined()) 40 | shouldAdvertise = true 41 | } 42 | 43 | /// Stops advertising peripheral. 44 | internal func stopAdvertising() { 45 | peripheralManager.stopAdvertising() 46 | peripheral?.configuration.services 47 | .compactMap { $0.advertisementService } 48 | .forEach { peripheralManager.remove($0) } 49 | shouldAdvertise = false 50 | } 51 | 52 | /// Updates a value on given characteristic. 53 | internal func updateValue(_ value: Data, characteristic: Characteristic, errorHandler: ((AdvertisementError) -> ())?) { 54 | guard let advertisementCharacteristic = characteristic.advertisementCharacteristic else { 55 | errorHandler?(.deviceNotAdvertising) 56 | return 57 | } 58 | peripheralManager.updateValue(value, for: advertisementCharacteristic, onSubscribedCentrals: subsribedCentrals) 59 | } 60 | } 61 | 62 | extension AdvertisementService: CBPeripheralManagerDelegate { 63 | 64 | /// - SeeAlso: `CBPeripheralManagerDelegate` 65 | func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { 66 | do { 67 | try peripheral.validateState() 68 | if !peripheralManager.isAdvertising && shouldAdvertise { 69 | peripheralManager.startAdvertising(self.peripheral?.advertisementData?.combined()) 70 | } 71 | } catch let error { 72 | guard let error = error as? BluetoothError else { return } 73 | errorHandler?(.bluetoothError(error)) 74 | } 75 | } 76 | 77 | /// - SeeAlso: `CBPeripheralManagerDelegate` 78 | func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { 79 | if let error = error { 80 | errorHandler?(.otherError(error)) 81 | return 82 | } 83 | self.peripheral?.configuration.services.map({ $0.assignAdvertisementService() }).forEach(peripheralManager.add(_:)) 84 | } 85 | 86 | /// - SeeAlso: `CBPeripheralManagerDelegate` 87 | func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { 88 | if let error = error { 89 | errorHandler?(.otherError(error)) 90 | } 91 | } 92 | 93 | /// - SeeAlso: `CBPeripheralManagerDelegate` 94 | func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { 95 | let rawCharacteristic = request.characteristic 96 | guard let characteristic = self.peripheral?.configuration.characteristic(matching: rawCharacteristic) else { return } 97 | guard let data = readCallback?(characteristic) else { return } 98 | request.value = data 99 | peripheral.respond(to: request, withResult: .success) 100 | } 101 | 102 | /// - SeeAlso: `CBPeripheralManagerDelegate` 103 | func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { 104 | requests.forEach { request in 105 | let rawCharacteristic = request.characteristic 106 | guard let characteristic = self.peripheral?.configuration.characteristic(matching: rawCharacteristic) else { return } 107 | writeCallback?(characteristic, request.value) 108 | peripheral.respond(to: request, withResult: .success) 109 | } 110 | } 111 | 112 | /// - SeeAlso: `CBPeripheralManagerDelegate` 113 | func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { 114 | subsribedCentrals.append(central) 115 | } 116 | 117 | /// - SeeAlso: `CBPeripheralManagerDelegate` 118 | func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { 119 | guard let index = subsribedCentrals.firstIndex(where: { $0 === central }) else { return } 120 | subsribedCentrals.remove(at: index) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Framework/Source Files/Advertisement/BluetoothAdvertisement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | /// A public facing class used to setup device as an advertising Bluetooth peripheral. 9 | public final class BluetoothAdvertisement { 10 | 11 | /// Advertisement service. 12 | /// - SeeAlso: `AdvertisementService` 13 | private lazy var advertisementService = AdvertisementService() 14 | 15 | /// A singleton instance. 16 | public static let shared = BluetoothAdvertisement() 17 | 18 | /// Default initializer 19 | public init() { } 20 | 21 | /// Start advertising peripheral with parameters given by a configuration of passed peripheral. 22 | /// 23 | /// - Parameters: 24 | /// - peripheral: a peripheral containing configuration with specified services and characteristics. 25 | /// - errorHandler: an error handler. Will be called only after unsuccessfull advertisement setup. 26 | /// - SeeAlso: `AdvertisementError` 27 | /// - SeeAlso: `Peripheral` 28 | public func advertise(peripheral: Peripheral, errorHandler: ((AdvertisementError) -> ())?) { 29 | advertisementService.startAdvertising(peripheral, errorHandler: errorHandler) 30 | } 31 | 32 | /// Stops advertising peripheral. 33 | public func stopAdvertising() { 34 | advertisementService.stopAdvertising() 35 | } 36 | 37 | /// Updates a value for specified characteristic with data. 38 | /// After the request a notify will be called on all subscribed centrals. 39 | /// 40 | /// - Parameters: 41 | /// - command: a comand to update on characteristic. 42 | /// - characteristic: specified characteristic to be updated. 43 | /// - errorHandler: an error handler called if data update fails. 44 | /// - SeeAlso: `AdvertisementError` 45 | /// - SeeAlso: `Characteristic` 46 | public func update(_ command: Command, characteristic: Characteristic, errorHandler: @escaping (AdvertisementError) -> (Void)) { 47 | do { 48 | try advertisementService.updateValue(command.convertedData(), characteristic: characteristic, errorHandler: errorHandler) 49 | } catch { 50 | errorHandler(.incorrectUpdateData) 51 | } 52 | } 53 | 54 | /// Caled when a connected central submits a write reuqest to a specified characteristic. 55 | public var writeRequestCallback: ((Characteristic, Data?) -> ())? { 56 | didSet { 57 | advertisementService.writeCallback = writeRequestCallback 58 | } 59 | } 60 | 61 | /// Called when a connected central submits a read request to a specified characteristic. 62 | public var readRequestCallback: ((Characteristic) -> (Data))? { 63 | didSet { 64 | advertisementService.readCallback = readRequestCallback 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Framework/Source Files/Command/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | /// Command enum - a handy wrapper for creating requests to peripheral devices. 10 | /// Handles creation of data with length according to passed parameter type, also creates data straight from hexadecimal 11 | /// string with exact the same value. If none of the cases matches needed type, use .data(Data) case where Data object can be passed directly. 12 | public enum Command { 13 | case int8(UInt8) 14 | case int16(UInt16) 15 | case int32(UInt32) 16 | case utf8String(String) 17 | case hexString(String) 18 | case data(Data) 19 | 20 | /// An error triggered when data parse fails. 21 | public enum ConversionError: Error { 22 | case incorrectInputFormat 23 | } 24 | } 25 | 26 | internal extension Command { 27 | 28 | /// Variable used for conversion of parameters to Data possible to write to peripheral. 29 | /// - Throws: Command.ConversionError 30 | func convertedData() throws -> Data { 31 | switch self { 32 | case .int8(let number): 33 | return number.decodedData 34 | case .int16(let number): 35 | return number.decodedData 36 | case .int32(let number): 37 | return number.decodedData 38 | case .utf8String(let string): 39 | guard let data = string.data(using: .utf8, allowLossyConversion: false) else { 40 | throw ConversionError.incorrectInputFormat 41 | } 42 | return data 43 | case .hexString(let string): 44 | return try string.hexDecodedData() 45 | case .data(let data): 46 | return data 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Framework/Source Files/Connection/BluetoothConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// Public facing interface granting methods to connect and disconnect devices. 10 | public final class BluetoothConnection: NSObject { 11 | 12 | /// A singleton instance. 13 | public static let shared = BluetoothConnection() 14 | 15 | /// Connection service implementing native CoreBluetooth stack. 16 | private var connectionService = ConnectionService() 17 | 18 | /// An advertisement validation handler. Will be called upon every peripheral discovery. Return value from this closure 19 | /// will indicate if manager should or shouldn't start connection with the passed peripheral according to it's identifier 20 | /// and advertising packet. 21 | @available(*, deprecated, message: "This closure will be removed in future version. Please use `peripheralValidationHandler`.") 22 | public var advertisementValidationHandler: ((Peripheral, String, [String: Any]) -> (Bool))? { 23 | didSet { 24 | connectionService.advertisementValidationHandler = advertisementValidationHandler 25 | } 26 | } 27 | 28 | /// A peripheral validation handler. Will be called upon every peripheral discovery. Contains matched peripheral, 29 | /// discovered peripheral from CoreBluetooth, advertisement data and RSSI value. Return value from this closure 30 | /// will indicate if manager should or shouldn't start connection with the passed peripheral according to it's identifier 31 | /// and advertising packet. 32 | public var peripheralValidationHandler: ((Peripheral, CBPeripheral, [String: Any], NSNumber) -> (Bool))? { 33 | didSet { 34 | connectionService.peripheralValidationHandler = peripheralValidationHandler 35 | } 36 | } 37 | 38 | /// Callback for update to Bluetooth sensor state for current device. 39 | /// Assign custom block to this property to monitor Central Manager state changes (`poweredOn`, `poweredOff`, `unauthorized` etc.). 40 | public var centralManagerStateUpdateHandler: ((CBManagerState) -> Void)? { 41 | didSet { 42 | connectionService.centralManagerStateUpdateHandler = centralManagerStateUpdateHandler 43 | } 44 | } 45 | 46 | /// A peripheral connection cancelled handler. Called when disconnecting a peripheral using `disconnect(_:)` is completed. 47 | /// Contains matched peripheral and native peripheral from CoreBluetooth. 48 | public var peripheralConnectionCancelledHandler: ((Peripheral, CBPeripheral) -> Void)? { 49 | didSet { 50 | connectionService.peripheralConnectionCancelledHandler = peripheralConnectionCancelledHandler 51 | } 52 | } 53 | 54 | /// Current Bluetooth authorization status. 55 | public var bluetoothAuthorizationStatus: BluetoothAuthorizationStatus { 56 | connectionService.bluetoothAuthorizationStatus 57 | } 58 | 59 | /// Primary method used to connect to a device. Can be called multiple times to connect more than on device at the same time. 60 | /// 61 | /// - Parameters: 62 | /// - peripheral: a configured device you wish to connect to. 63 | /// - handler: a completion handler called upon successfull connection or a error. 64 | /// - SeeAlso: `BluetoothConnection.ConnectionError` 65 | /// - SeeAlso: `Peripheral` 66 | public func connect(_ peripheral: Peripheral, handler: ((ConnectionError?) -> ())?) { 67 | guard !peripheral.isConnected else { 68 | handler?(.deviceAlreadyConnected) 69 | return 70 | } 71 | guard !connectionService.exceededDevicesConnectedLimit else { 72 | handler?(.deviceConnectionLimitExceed) 73 | return 74 | } 75 | connectionService.connect(peripheral) { (peripheral, error) in 76 | guard peripheral === peripheral else { return } 77 | handler?(error) 78 | } 79 | } 80 | 81 | /// Primary method to disconnect a device. If it's not yet connected it'll be removed from connection queue, and connection attempts will stop. 82 | /// 83 | /// - Parameter peripheral: a peripheral you wish to disconnect. Should be exactly the same instance that was used for connection. 84 | public func disconnect(_ peripheral: Peripheral) { 85 | guard let cbPeripheral = peripheral.peripheral else { 86 | connectionService.remove(peripheral) 87 | return 88 | } 89 | connectionService.disconnect(cbPeripheral) 90 | } 91 | 92 | /// Function called to stop scanning for devices. 93 | public func stopScanning() { 94 | connectionService.stopScanning() 95 | } 96 | 97 | /// Requests User for authorization to use Bluetooth. 98 | public func requestBluetoothAuthorization() { 99 | connectionService.requestBluetoothAuthorization() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Framework/Source Files/Connection/ConnectionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// A wrapper around CoreBluetooth delegate stack. 10 | internal final class ConnectionService: NSObject { 11 | 12 | /// Closure used to check given peripheral against advertisement packet of discovered peripheral. 13 | internal var advertisementValidationHandler: ((Peripheral, String, [String: Any]) -> (Bool))? = { _,_,_ in return true } 14 | 15 | /// Closure used to check given peripheral against advertisement packet of discovered peripheral. 16 | internal var peripheralValidationHandler: ((Peripheral, CBPeripheral, [String: Any], NSNumber) -> (Bool))? = { _,_,_,_ in return true } 17 | 18 | /// Closure used to manage connection success or failure. 19 | internal var connectionHandler: ((Peripheral, ConnectionError?) -> ())? 20 | 21 | /// Closure called when disconnecting a peripheral using `disconnect(_:)` is completed. 22 | internal var peripheralConnectionCancelledHandler: ((Peripheral, CBPeripheral) -> ())? 23 | 24 | /// Called when Bluetooth sensor state for current device was updated. 25 | internal var centralManagerStateUpdateHandler: ((CBManagerState) -> Void)? 26 | 27 | /// Returns the amount of devices already connected. 28 | internal var connectedDevicesAmount: Int { 29 | return peripherals.read { storage in 30 | storage 31 | .filter { $0.isConnected } 32 | .count 33 | } 34 | } 35 | 36 | /// Indicates whether connected devices limit has been exceeded. 37 | internal var exceededDevicesConnectedLimit: Bool { 38 | return connectedDevicesAmount >= deviceConnectionLimit 39 | } 40 | 41 | /// Current Bluetooth authorization status. 42 | internal var bluetoothAuthorizationStatus: BluetoothAuthorizationStatus { 43 | if #available(iOS 13.1, *) { 44 | return CBManager.authorization.bluetoothAuthorizationStatus 45 | } else if #available(iOS 13.0, *) { 46 | return centralManager.authorization.bluetoothAuthorizationStatus 47 | } else { 48 | // Until iOS 12 applications could access Bluetooth without the user’s authorization 49 | return .allowedAlways 50 | } 51 | } 52 | 53 | /// Maximum amount of devices capable of connecting to a iOS device. 54 | private let deviceConnectionLimit = 8 55 | 56 | /// Set of peripherals the manager should connect. 57 | private var peripherals: SynchronizedArray> = [] 58 | 59 | /// Handle to peripherals which were requested to disconnect. 60 | private var peripheralsToDisconnect: SynchronizedArray> = [] 61 | 62 | private weak var connectingPeripheral: Peripheral? 63 | 64 | /// Connection options - means you will be notified on connection and disconnection of devices. 65 | private lazy var connectionOptions = [CBConnectPeripheralOptionNotifyOnConnectionKey: true, 66 | CBConnectPeripheralOptionNotifyOnDisconnectionKey: true] 67 | 68 | /// Scanning options. Means that one device can be discovered multiple times without connecting. 69 | private lazy var scanningOptions = [CBCentralManagerScanOptionAllowDuplicatesKey : true] 70 | 71 | /// CBCentralManager instance. Allows peripheral connection. 72 | /// iOS displays Bluetooth authorization popup when `CBCentralManager` is instantiated and authorization status is not determined. 73 | private lazy var centralManager: CBCentralManager = { 74 | let manager = CBCentralManager() 75 | manager.delegate = self 76 | return manager 77 | }() 78 | 79 | /// Set of advertisement UUID central manager should scan for. 80 | private var scanParameters: Set = Set() 81 | } 82 | 83 | extension ConnectionService { 84 | 85 | /// Starts connection with passed device. Connection result is passed in handler closure. 86 | internal func connect(_ peripheral: Peripheral, handler: @escaping (Peripheral, ConnectionError?) -> ()) { 87 | if connectionHandler == nil { 88 | connectionHandler = handler 89 | } 90 | do { 91 | try centralManager.validateState() 92 | peripherals.append(peripheral) 93 | reloadScanning() 94 | } catch let error { 95 | if let error = error as? BluetoothError { 96 | handler(peripheral, .bluetoothError(error)) 97 | } 98 | } 99 | } 100 | 101 | /// Disconnects given device. 102 | internal func disconnect(_ peripheral: CBPeripheral) { 103 | peripherals.write { storage in 104 | guard let index = storage.firstIndex(where: { $0.peripheral === peripheral }) else { return } 105 | let peripheralToDisconnect = storage.remove(at: index) 106 | self.peripheralsToDisconnect.append(peripheralToDisconnect) 107 | } 108 | centralManager.cancelPeripheralConnection(peripheral) 109 | } 110 | 111 | /// Function called to remove peripheral from queue 112 | /// - Parameter peripheral: peripheral to remove. 113 | internal func remove(_ peripheral: Peripheral) { 114 | peripherals.write { storage in 115 | guard let index = storage.firstIndex(where: { $0 === peripheral }) else { return } 116 | storage.remove(at: index) 117 | } 118 | } 119 | 120 | /// Function called to stop scanning for devices. 121 | internal func stopScanning() { 122 | centralManager.stopScan() 123 | } 124 | 125 | /// Triggers showing Bluetooth authorization popup by instantiating the central manager. 126 | internal func requestBluetoothAuthorization() { 127 | _ = centralManager 128 | } 129 | } 130 | 131 | private extension ConnectionService { 132 | 133 | /// Reloads scanning if necessary. Adding scan parameters should be ommited in case of possible peripheral retrive. 134 | /// In that case connection is available without previous scanning. 135 | private func reloadScanning() { 136 | if centralManager.isScanning { 137 | centralManager.stopScan() 138 | } 139 | performDeviceAutoReconnection() 140 | let params: [CBUUID] = peripherals.read { storage in 141 | storage.compactMap { (peripheral) -> CBUUID? in 142 | guard peripheral.peripheral == nil else { return nil } 143 | return peripheral.configuration.advertisementUUID 144 | } 145 | } 146 | guard params.count != 0 else { 147 | return 148 | } 149 | scanParameters = Set(params) 150 | guard case .poweredOn = centralManager.state else { return } 151 | centralManager.scanForPeripherals(withServices: Array(scanParameters), options: scanningOptions) 152 | } 153 | 154 | /// Tries a peripeheral retrieve for each peripheral model. Peripheral can be retrieved in case the parameter of 155 | /// deviceIdentifier was passed during initialization. If it's correctly retrieved, scanning is unnecessary and peripheral 156 | /// can be directly connected. 157 | private func performDeviceAutoReconnection() { 158 | peripherals.read { storage in 159 | let identifiers = storage 160 | .filter { !$0.isConnected } 161 | .compactMap { UUID(uuidString: $0.deviceIdentifier ?? "") } 162 | guard !identifiers.isEmpty else { return } 163 | let retrievedPeripherals = centralManager.retrievePeripherals(withIdentifiers: identifiers) 164 | let matching = storage.matchingElementsWith(retrievedPeripherals) 165 | matching.forEach { (peripheral, cbPeripheral) in 166 | peripheral.peripheral = cbPeripheral 167 | centralManager.connect(cbPeripheral, options: connectionOptions) 168 | } 169 | } 170 | } 171 | } 172 | 173 | extension ConnectionService: CBCentralManagerDelegate { 174 | 175 | /// Determines Bluetooth sensor state for current device. 176 | /// - SeeAlso: CBCentralManagerDelegate 177 | public func centralManagerDidUpdateState(_ central: CBCentralManager) { 178 | centralManagerStateUpdateHandler?(central.state) 179 | 180 | guard let handler = connectionHandler, 181 | let anyDevice = peripherals.first 182 | else { return } 183 | 184 | do { 185 | try central.validateState() 186 | reloadScanning() 187 | } catch let error { 188 | if let error = error as? BluetoothError { 189 | handler(anyDevice, .bluetoothError(error)) 190 | } 191 | } 192 | } 193 | 194 | /// Called when a peripheral with desired advertised service is discovered. 195 | /// - SeeAlso: CBCentralManagerDelegate 196 | public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 197 | let matchingPeripheral = peripherals.read { storage in 198 | storage 199 | .filter({ $0.configuration.matches(advertisement: advertisementData)}) 200 | .first(where: { $0.peripheral == nil }) 201 | } 202 | 203 | guard let handler = peripheralValidationHandler, 204 | let matchingPeripheral, 205 | connectingPeripheral == nil, 206 | handler(matchingPeripheral, peripheral, advertisementData, RSSI) 207 | else { return } 208 | 209 | connectingPeripheral = matchingPeripheral 210 | connectingPeripheral?.peripheral = peripheral 211 | connectingPeripheral?.rssi = RSSI 212 | central.connect(peripheral, options: connectionOptions) 213 | if exceededDevicesConnectedLimit { 214 | centralManager.stopScan() 215 | } 216 | } 217 | 218 | /// Called upon a successfull peripheral connection. 219 | /// - SeeAlso: CBCentralManagerDelegate 220 | public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 221 | let connectingPeripheral = peripherals.read { $0.first(withIdentical: peripheral) } 222 | guard let connectingPeripheral else { 223 | // Central manager did connect to a peripheral, which is not on the list of allowed peripherals at this moment. 224 | // Peripheral might have re-connected unexpectedly. Disconnect it, so it can be discovered. 225 | centralManager.cancelPeripheralConnection(peripheral) 226 | return 227 | } 228 | self.connectingPeripheral = connectingPeripheral 229 | connectingPeripheral.peripheral = peripheral 230 | peripheral.delegate = self 231 | peripheral.discoverServices(connectingPeripheral.configuration.services.map({ $0.bluetoothUUID })) 232 | } 233 | 234 | /// Called when peripheral connection fails on its initialization, we'll reconnect it right away. 235 | /// - SeeAlso: CBCentralManagerDelegate 236 | public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 237 | central.connect(peripheral, options: connectionOptions) 238 | } 239 | } 240 | 241 | extension ConnectionService: CBPeripheralDelegate { 242 | 243 | /// Called upon discovery of services of a connected peripheral. Used to map model services to passed configuration and 244 | /// discover characteristics for each matching service. 245 | /// - SeeAlso: CBPeripheralDelegate 246 | public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 247 | guard let services = peripheral.services, error == nil else { return } 248 | let matching = connectingPeripheral?.configuration.services.matchingElementsWith(services) 249 | guard matching?.count != 0 else { 250 | centralManager.cancelPeripheralConnection(peripheral) 251 | return 252 | } 253 | matching?.forEach({ (service, cbService) in 254 | peripheral.discoverCharacteristics(service.characteristics.map({ $0.bluetoothUUID }), for: cbService) 255 | }) 256 | } 257 | 258 | /// Called upon discovery of characteristics of a connected peripheral per each passed service. Used to map CBCharacteristic 259 | /// instances to passed configuration, assign characteristic raw values and setup notifications. 260 | /// - SeeAlso: CBPeripheralDelegate 261 | public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 262 | guard let characteristics = service.characteristics, error == nil else { return } 263 | let matchingService = connectingPeripheral?.configuration.services.first(where: { $0.bluetoothUUID == service.uuid }) 264 | let matchingCharacteristics = matchingService?.characteristics.matchingElementsWith(characteristics) 265 | guard matchingCharacteristics?.count != 0 else { 266 | centralManager.cancelPeripheralConnection(peripheral) 267 | return 268 | } 269 | matchingCharacteristics?.forEach({ (tuple) in 270 | let (characteristic, cbCharacteristic) = tuple 271 | characteristic.setRawCharacteristic(cbCharacteristic) 272 | peripheral.setNotifyValue(characteristic.isObservingValue, for: cbCharacteristic) 273 | }) 274 | if let connectingPeripheral = self.connectingPeripheral { 275 | connectingPeripheral.peripheral?.delegate = connectingPeripheral 276 | self.connectionHandler?(connectingPeripheral, nil) 277 | } 278 | connectingPeripheral = nil 279 | } 280 | 281 | /// Called when device is disconnected. 282 | /// If connection was cancelled using `disconnect(_:)`, then `peripheralConnectionCancelledHandler(_:)` is called. 283 | /// Otherwise device is reconnected. Connect method does not have a timeout, so connection will be triggered 284 | /// anytime in the future when the device is discovered. In case the connection is no longer needed we'll just return. 285 | /// - SeeAlso: CBPeripheralDelegate 286 | public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 287 | /// `error` is nil if disconnect resulted from a call to `cancelPeripheralConnection(_:)`. 288 | /// SeeAlso: https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518791-centralmanager 289 | if error == nil { 290 | peripheralsToDisconnect.write { [weak self] storage in 291 | guard let disconnectedPeripheral = storage.first(withIdentical: peripheral) else { return } 292 | storage.removeAll(where: { $0 === disconnectedPeripheral }) 293 | self?.peripheralConnectionCancelledHandler?(disconnectedPeripheral, peripheral) 294 | } 295 | return 296 | } 297 | 298 | if let disconnectedPeripheral = peripherals.read({ $0.first(withIdentical: peripheral) }), 299 | let nativePeripheral = disconnectedPeripheral.peripheral { 300 | disconnectedPeripheral.disconnectionHandler?() 301 | centralManager.connect(nativePeripheral, options: connectionOptions) 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | internal extension Array { 10 | 11 | /// Utility method used for matching elements from two different arrays and returning an array of tuples. Matching is based 12 | /// on a return value from comparison handler. 13 | func matchingElementsWith(_ array: [T], comparison: (Element, T) -> Bool) -> [(Element, T)] { 14 | return compactMap { element in 15 | guard let index = array.firstIndex(where: { comparison(element, $0) }) else { return nil } 16 | return (element, array[index]) 17 | } 18 | } 19 | } 20 | 21 | internal extension Array where Element == Service { 22 | 23 | /// Convenience method for easier matching of Service and CBService objects. 24 | func matchingElementsWith(_ array: [CBService]) -> [(Service, CBService)] { 25 | return matchingElementsWith(array, comparison: { (service, cbService) -> Bool in 26 | return cbService.uuid == service.bluetoothUUID 27 | }) 28 | } 29 | } 30 | 31 | internal extension Array where Element == Characteristic { 32 | 33 | /// Convenience method for easier matching of Characteristic and CBCharacteristic objects. 34 | func matchingElementsWith(_ array: [CBCharacteristic]) -> [(Characteristic, CBCharacteristic)] { 35 | return matchingElementsWith(array, comparison: { (characteristic, cbCharacteristic) -> Bool in 36 | return characteristic.bluetoothUUID == cbCharacteristic.uuid 37 | }) 38 | } 39 | } 40 | 41 | internal extension Array where Element == Peripheral { 42 | 43 | /// Convenience method for easier matching of Peripheral and CBPeripheral objects. 44 | func matchingElementsWith(_ array: [CBPeripheral]) -> [(Peripheral, CBPeripheral)] { 45 | return matchingElementsWith(array, comparison: { (peripheral, cbPeripheral) -> Bool in 46 | return peripheral.deviceIdentifier == cbPeripheral.identifier.uuidString 47 | }) 48 | } 49 | } 50 | 51 | internal extension Array where Element == AdvertisementData { 52 | 53 | /// Convenience method used to generate advertising packet from array of AdvertisementData objects. 54 | func combined() -> [String: Any] { 55 | var dictionary = [String: Any]() 56 | forEach { 57 | guard let data = $0.data else { return } 58 | var assignedValue: Any = $0 59 | if let currentValue = dictionary[$0.key], let newValue = data as? [Any] { 60 | if var arrayValue = currentValue as? [Any] { 61 | arrayValue.append(contentsOf: newValue) 62 | assignedValue = arrayValue 63 | } else { 64 | assignedValue = [data, currentValue] 65 | } 66 | } else { 67 | assignedValue = data 68 | } 69 | dictionary[$0.key] = assignedValue 70 | } 71 | return dictionary 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/CBCharacteristic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth.CBCharacteristic 8 | 9 | internal extension CBCharacteristic { 10 | 11 | /// Validates if given characteristic is readable. 12 | func validateForRead() -> Bool { 13 | return properties.contains(.read) 14 | } 15 | 16 | /// Validates if given characteristic is writeable. 17 | func validateForWrite() -> Bool { 18 | return properties.contains(.write) || properties.contains(.writeWithoutResponse) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/CBManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | internal extension CBManager { 10 | 11 | /// Validates the current state of CBManager class to determine if Bluetooth is not supported on this device or is turned off or unavailable for some other reason. 12 | func validateState() throws { 13 | switch state { 14 | case .poweredOff, .resetting: 15 | throw BluetoothError.bluetoothUnavailable 16 | case .unauthorized: 17 | throw BluetoothError.unauthorized 18 | case .unsupported, .unknown: 19 | throw BluetoothError.incompatibleDevice 20 | default: 21 | break 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/CBUUID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth.CBUUID 8 | 9 | internal extension CBUUID { 10 | 11 | /// Error for creating a CBUUID with string invalid to UUID standards. 12 | enum CreationError: Error { 13 | case invalidString 14 | } 15 | 16 | /// Convenience initializer, a wrapper for default init(string: String) method with error handling, not crashing 17 | /// like default one. 18 | /// 19 | /// - Parameter uuidString: a String wished to be converted into CBUIID. 20 | /// - Throws: `CreationError.invalidString` if passed String is not valid. 21 | convenience init(uuidString: String) throws { 22 | guard let uuid = UUID(uuidString: uuidString) else { 23 | guard uuidString.isValidShortenedUUID() else { throw CreationError.invalidString } 24 | self.init(string: uuidString) 25 | return 26 | } 27 | self.init(nsuuid: uuid) 28 | } 29 | } 30 | 31 | /// Handy extension used to be able to compare CBUUID's with == operator. 32 | extension CBUUID: Comparable { 33 | public static func <(lhs: CBUUID, rhs: CBUUID) -> Bool { 34 | return lhs.uuidString == rhs.uuidString 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | /// Data extension allowing conversion from Data into String containing exactly the same bytes. 9 | internal extension Data { 10 | 11 | /// Returns encoded String. 12 | var hexEncodedString: String { 13 | return map { String(format: "%02hhX", $0) }.joined() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/Int.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | /// Extension for signed integers allowing conversion to Data with proper size. 9 | internal extension UnsignedInteger { 10 | 11 | /// Returns decoded data with proper size. 12 | var decodedData: Data { 13 | return withUnsafeBytes(of: self) { Data($0) } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/Sequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence.swift 3 | // Bluetooth 4 | // 5 | // Created by Filip Zieliński on 08/04/2022. 6 | // Copyright © 2022 Netguru. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | internal extension Sequence { 13 | 14 | /// Returns the first element of the sequence where given element property is identical (`===`) to given object instance. 15 | /// - Parameters: 16 | /// - propertyKeyPath: a key path to an element property. 17 | /// - instance: an object instance to compare against. 18 | /// - Returns: The first element of the sequence where given element property is identical (`===`) to given object instance, or `nil` if there is no such element. 19 | func first(where propertyKeyPath: KeyPath, isIdenticalTo instance: Object) -> Element? { 20 | first { $0[keyPath: propertyKeyPath] === instance } 21 | } 22 | 23 | /// Returns the first element of the sequence where given element optional property is identical (`===`) to given object instance. 24 | /// - Parameters: 25 | /// - propertyKeyPath: a key path to an element property. 26 | /// - instance: an object instance to compare against. 27 | /// - Returns: The first element of the sequence where given element optional property is identical (`===`) to given object instance, or `nil` if there is no such element. 28 | func first(where propertyKeyPath: KeyPath, isIdenticalTo instance: Object) -> Element? { 29 | first { $0[keyPath: propertyKeyPath] === instance } 30 | } 31 | } 32 | 33 | internal extension Sequence where Element == Peripheral { 34 | 35 | /// Convenience method returning the first peripheral of the sequence with `peripheral` property identical (`===`) to given `CBPeripheral` instance. 36 | /// - Parameter cbPeripheral: a `CBPeripheral` instance. 37 | /// - Returns: The first peripheral of the sequence with `peripheral` property identical (`===`) to given `CBPeripheral` instance, or `nil` if there is no such element. 38 | func first(withIdentical cbPeripheral: CBPeripheral) -> Element? { 39 | first(where: \.peripheral, isIdenticalTo: cbPeripheral) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Framework/Source Files/Extensions/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | internal extension String { 9 | 10 | /// Validates if String is a proper shoretened UUID which means its 4 or 6 characters long and contains only hexadecimal characters. 11 | func isValidShortenedUUID() -> Bool { 12 | var evaluation = self 13 | if hasPrefix("0x") { 14 | evaluation = evaluation.replacingOccurrences(of: "0x", with: "") 15 | } 16 | return isHexadecimal && (evaluation.count == 4 || evaluation.count == 6) 17 | } 18 | 19 | /// Checks if string contains only hexadecimal characters. 20 | var isHexadecimal: Bool { 21 | let invertedHexCharacterSet = NSCharacterSet(charactersIn: "0123456789ABCDEF").inverted 22 | return uppercased().rangeOfCharacter(from: invertedHexCharacterSet) == nil 23 | } 24 | } 25 | 26 | /// String extension allowing conversion of strings like 0x2A01 into Data with the same format. 27 | internal extension String { 28 | 29 | /// Returns Data with decoded string. 30 | func hexDecodedData() throws -> Data { 31 | guard isHexadecimal else { 32 | throw Command.ConversionError.incorrectInputFormat 33 | } 34 | var data = Data(capacity: count / 2) 35 | let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) 36 | regex.enumerateMatches(in: self, range: NSMakeRange(0, utf16.count)) { match, _, _ in 37 | guard let nsRange = match?.range, let range = Range(nsRange, in: self) else { return } 38 | let byteString = self[range] 39 | guard var num = UInt8(byteString, radix: 16) else { return } 40 | data.append(&num, count: 1) 41 | } 42 | return data 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/AdvertisementData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// An enum used to specify advertising parameters of a peripheral. 10 | public enum AdvertisementData { 11 | 12 | case localName(String) 13 | case servicesUUIDs(String) 14 | case serviceData(Data) 15 | case txPower(Int) 16 | case manufacturersData(String) 17 | case custom(String, Any) 18 | } 19 | 20 | internal extension AdvertisementData { 21 | 22 | var data: Any? { 23 | switch self { 24 | case .localName(let name): 25 | return name 26 | case .servicesUUIDs(let uuid): 27 | return try? [CBUUID(uuidString: uuid)] 28 | case .serviceData(let data): 29 | return data 30 | case .txPower(let level): 31 | return level 32 | case .manufacturersData(let data): 33 | return data 34 | case .custom(_, let data): 35 | return data 36 | } 37 | } 38 | 39 | var key: String { 40 | switch self { 41 | case .localName(_): 42 | return CBAdvertisementDataLocalNameKey 43 | case .servicesUUIDs(_): 44 | return CBAdvertisementDataServiceUUIDsKey 45 | case .serviceData(_): 46 | return CBAdvertisementDataServiceDataKey 47 | case .txPower(_): 48 | return CBAdvertisementDataTxPowerLevelKey 49 | case .manufacturersData(_): 50 | return CBAdvertisementDataManufacturerDataKey 51 | case .custom(let key, _): 52 | return key 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/BluetoothAuthorizationStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import CoreBluetooth 7 | 8 | /// The current authorization state of a Core Bluetooth manager. 9 | @objc public enum BluetoothAuthorizationStatus: Int { 10 | /// Bluetooth authorization status is not determined. 11 | case notDetermined = 0 12 | 13 | /// Bluetooth connection is restricted. 14 | case restricted = 1 15 | 16 | /// Bluetooth connection is denied. 17 | case denied = 2 18 | 19 | /// Bluetooth connection is allowed always. 20 | case allowedAlways = 3 21 | } 22 | 23 | extension BluetoothAuthorizationStatus { 24 | 25 | /// `CBManagerAuthorization` representation of current authorization status. 26 | @available(iOS 13.0, *) 27 | var cbManagerAuthorization: CBManagerAuthorization { 28 | switch self { 29 | case .notDetermined: 30 | return .notDetermined 31 | case .restricted: 32 | return .restricted 33 | case .denied: 34 | return .denied 35 | case .allowedAlways: 36 | return .allowedAlways 37 | } 38 | } 39 | } 40 | 41 | @available(iOS 13.0, *) 42 | extension CBManagerAuthorization { 43 | 44 | /// `BluetoothAuthorizationStatus` representation of current authorization status. 45 | @available(iOSApplicationExtension 13.0, *) 46 | var bluetoothAuthorizationStatus: BluetoothAuthorizationStatus { 47 | switch self { 48 | case .notDetermined: 49 | return .notDetermined 50 | case .restricted: 51 | return .restricted 52 | case .denied: 53 | return .denied 54 | case .allowedAlways: 55 | return .allowedAlways 56 | @unknown default: 57 | return .notDetermined 58 | } 59 | } 60 | } 61 | 62 | extension BluetoothAuthorizationStatus: CustomStringConvertible { 63 | 64 | /// SeeAlso: `CustomStringConvertible.description` 65 | public var description: String { 66 | switch self { 67 | case .notDetermined: 68 | return "notDetermined" 69 | case .restricted: 70 | return "restricted" 71 | case .denied: 72 | return "denied" 73 | case .allowedAlways: 74 | return "allowedAlways" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/BluetoothError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | /// List of possible general Bluetooth failures. 9 | public enum BluetoothError: Error { 10 | case bluetoothUnavailable 11 | case incompatibleDevice 12 | case unauthorized 13 | } 14 | 15 | /// List of possible errors during advertisement. 16 | public enum AdvertisementError: Error { 17 | case bluetoothError(BluetoothError) 18 | case deviceNotAdvertising 19 | case incorrectUpdateData 20 | case otherError(Error) 21 | } 22 | 23 | /// List of possible errors during connection. 24 | public enum ConnectionError: Error { 25 | case bluetoothError(BluetoothError) 26 | case deviceConnectionLimitExceed 27 | case deviceAlreadyConnected 28 | } 29 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/Characteristic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// Struct wrapping Apple's native CBCharacteristic class. Used to create Configuration to connect with peripheral. 10 | /// It presents a clear interface for interacting with characteristics providing notify property. 11 | public class Characteristic { 12 | 13 | /// UUID of desired Characteristic. 14 | public let uuid: String 15 | 16 | /// A bool indicating if isNotifying value should be set on a characteristic upon discovery. 17 | public let isObservingValue: Bool 18 | 19 | /// A bool indicating a peripheral can write the characteristic’s value with a response. 20 | public var isWriteWithResponse: Bool? { 21 | return rawCharacteristic?.properties.contains(.write) 22 | } 23 | 24 | /// A handler indicating characteristic value update events. 25 | public var notifyHandler: ((Data?) -> ())? 26 | 27 | /// Raw characteristics value filled after connection. 28 | internal var rawCharacteristic: CBCharacteristic? 29 | 30 | /// Raw mutable characteristic, assigned for advertisement needs. 31 | internal var advertisementCharacteristic: CBMutableCharacteristic? 32 | 33 | /// CBUUID parsed from passed UUID String. 34 | internal let bluetoothUUID: CBUUID 35 | 36 | /// Initializes a new instance of Characteristic. It's failable if passed UUID String is not parseable to UUID standards. 37 | /// 38 | /// - Parameters: 39 | /// - uuid: UUID of desired service, should be parseable to CBUUID in order for the initializer to work. 40 | /// - shouldObserveNotification: indicates if this characteristic should notify when it's value changes. Note that this will happen only when characteristic properties include Notify. False by default. 41 | /// - Throws: CBUUID.CreationError 42 | /// - SeeAlso: `CBUUID.CreationError` 43 | public init(uuid: String, shouldObserveNotification: Bool = false) throws { 44 | self.bluetoothUUID = try CBUUID(uuidString: uuid) 45 | self.uuid = uuid 46 | self.isObservingValue = shouldObserveNotification 47 | } 48 | 49 | /// Sets raw characteristic used for notifying purposes 50 | internal func setRawCharacteristic(_ characteristic: CBCharacteristic) { 51 | rawCharacteristic = characteristic 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// Configuration struct is used to create a complete representation of peripheral's services and characteristics. 10 | /// Use to initialize Perpiheral class. 11 | public struct Configuration { 12 | 13 | /// An array of services contained in configuration. 14 | public let services: [Service] 15 | 16 | /// Advertised UUID from initializer parsed to a CBUUID instance. 17 | internal let advertisementUUID: CBUUID 18 | 19 | /// Creates a new instance of configuration containing Services desired peripheral should contain. 20 | /// Used to initialize a Peripheral instance. 21 | /// 22 | /// - Parameters: 23 | /// - services: An array of Services wished to use. 24 | /// - advertisement: UUID of desired peripheral that is specified in advertisement header. 25 | /// - Throws: CBUUID.CreationError 26 | /// - SeeAlso: `CBUUID.CreationError` 27 | public init(services: [Service], advertisement: String) throws { 28 | advertisementUUID = try CBUUID(uuidString: advertisement) 29 | self.services = services 30 | } 31 | } 32 | 33 | internal extension Configuration { 34 | 35 | /// Helper method to check peripheral advertisement against one passed in conifguration. 36 | func matches(advertisement: [String: Any]) -> Bool { 37 | guard let uuids = advertisement[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] else { return false } 38 | for uuid in uuids { 39 | if uuid.uuidString.uppercased() == advertisementUUID.uuidString.uppercased() { return true } 40 | } 41 | return false 42 | } 43 | 44 | /// Helper method used to search characteristics wrapper for specified CBCharacteristic 45 | func characteristic(matching cbCharacteristic: CBCharacteristic) -> Characteristic? { 46 | let characteristics = services.flatMap { $0.characteristics } 47 | return characteristics.filter { $0.bluetoothUUID.uuidString == cbCharacteristic.uuid.uuidString }.first 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/Peripheral/AdvertisablePeripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | public extension Peripheral where Type == Advertisable { 10 | 11 | /// Creates a new instance of Peripheral that will be used for advertisement purposes. 12 | /// 13 | /// - Parameters: 14 | /// - configuration: a specification of the peripheral that you are willing to advertise wrapped in Configuration object instance. 15 | /// - advertisementData: a data that should be put in Bluetooth LE advertisement header. Please note that iPhones don't allow some keys there, so they won't be advertised even if set properly. 16 | convenience init(configuration: Configuration, advertisementData: [AdvertisementData]) { 17 | self.init(configuration: configuration, deviceIdentifier: nil, advertisementData: advertisementData) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/Peripheral/ConnectablePeripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | public extension Peripheral where Type == Connectable { 10 | 11 | /// Enum for distinguishing which transmission action was taken. 12 | private enum TransmissionAction { 13 | case read, write 14 | } 15 | 16 | /// Deafult initializer for Perpipheral. 17 | /// 18 | /// - Parameters: 19 | /// - configuration: previously created configuration containing all desired services and characteristics. 20 | /// - deviceIdentifier: optional parameter. If device identifier is cached locally then it should be passed here. When set, connection to peripheral is much quicker. 21 | /// - SeeAlso: `Configuration` 22 | convenience init(configuration: Configuration, deviceIdentifier: String? = nil) { 23 | self.init(configuration: configuration, deviceIdentifier: deviceIdentifier, advertisementData: nil) 24 | } 25 | 26 | /// Indicates if device is currently connected. 27 | var isConnected: Bool { 28 | guard let peripheral = peripheral else { return false } 29 | return peripheral.state == .connected 30 | } 31 | 32 | /// Method used for writing to the peripheral after it's connected. 33 | /// 34 | /// - Parameters: 35 | /// - command: a command to write to the device. 36 | /// - characteristic: a characteristic the command should be directed to. 37 | /// - handler: a completion handler indicating if reuqest was successfull. 38 | /// - type: type of write request 39 | /// - SeeAlso: `Command` 40 | /// - SeeAlso: `Characteristic` 41 | /// - SeeAlso: `CBCharacteristicWriteType` 42 | /// - SeeAlso: `Peripheral.TransmissionError` 43 | func write(command: Command, characteristic: Characteristic, type: CBCharacteristicWriteType = .withResponse, handler: ((TransmissionError?) -> ())?) { 44 | do { 45 | let unwrapped = try validateForTransmission(characteristic, action: .write) 46 | writeHandler = handler 47 | try peripheral?.writeValue(command.convertedData(), for: unwrapped, type: type) 48 | } catch let error { 49 | guard let conversionError = error as? Command.ConversionError else { 50 | handler?(error as? TransmissionError) 51 | return 52 | } 53 | handler?(TransmissionError.incorrectInputFormat(conversionError)) 54 | } 55 | } 56 | 57 | /// Method used to perform read request from peripheral after it's connected. 58 | /// 59 | /// - Parameters: 60 | /// - characteristic: a characteristic you wish to read. 61 | /// - handler: completion handler returning Data retrieved from characteristic or error if it failed. 62 | /// - SeeAlso: `Characteristic` 63 | /// - SeeAlso: `Peripheral.TransmissionError` 64 | func read(_ characteristic: Characteristic, handler: ((Data?, TransmissionError?) -> ())?) { 65 | do { 66 | let unwrapped = try validateForTransmission(characteristic, action: .read) 67 | readHandler = handler 68 | peripheral?.readValue(for: unwrapped) 69 | } catch let error { 70 | handler?(nil, error as? TransmissionError) 71 | } 72 | } 73 | 74 | /// Method used to perform read RSSI request from peripheral. 75 | /// 76 | /// - Parameter handler: completion handler returning RSSI value retrieved from peripheral. 77 | func readRSSI(_ handler: ((NSNumber?, TransmissionError?) -> ())?) { 78 | guard isConnected else { 79 | handler?(nil, .deviceNotConnected) 80 | return 81 | } 82 | rssiHandler = handler 83 | peripheral?.readRSSI() 84 | } 85 | 86 | /// Performs a general validation if write or read requests can be performed on specified characteristic. 87 | private func validateForTransmission(_ characteristic: Characteristic, action: TransmissionAction) throws -> CBCharacteristic { 88 | guard isConnected else { 89 | throw TransmissionError.deviceNotConnected 90 | } 91 | guard let characteristic = characteristic.rawCharacteristic else { 92 | throw TransmissionError.characteristicNotDiscovered 93 | } 94 | if action == .read && !characteristic.validateForRead() { 95 | throw TransmissionError.invalidCharacteristicPermissions(characteristic.properties) 96 | } 97 | if action == .write && !characteristic.validateForWrite() { 98 | throw TransmissionError.invalidCharacteristicPermissions(characteristic.properties) 99 | } 100 | return characteristic 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/Peripheral/Peripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// A phantom type protocol making it easier to distinguish peripherals. 10 | public protocol PeripheralType {} 11 | 12 | /// A phantom type enums allowing to distinguish between Peripheral used for connection and advertisement. 13 | public enum Connectable: PeripheralType {} 14 | public enum Advertisable: PeripheralType {} 15 | 16 | /// Class wrapping native Apple's CBPeripheral class. Should be passed as connection parameter and initialized with a 17 | /// Configuration. It presents a clear interface for writing and reading interactions with remote peripherals adding closure reponses. 18 | public final class Peripheral: NSObject, CBPeripheralDelegate { 19 | 20 | /// List of errors possible to happen upon a write or read request. 21 | public enum TransmissionError: Error { 22 | case invalidCharacteristicPermissions(CBCharacteristicProperties) 23 | case characteristicNotDiscovered 24 | case deviceNotConnected 25 | case incorrectInputFormat(Command.ConversionError) 26 | case auxiliaryError(Error) 27 | } 28 | 29 | public override init() { 30 | fatalError("Init is unavailable, please use init(configuration:deviceIdentifier:) instead.") 31 | } 32 | 33 | internal init(configuration: Configuration, deviceIdentifier: String? = nil, advertisementData: [AdvertisementData]? = nil) { 34 | self.configuration = configuration 35 | self.deviceIdentifier = deviceIdentifier 36 | self.advertisementData = advertisementData 37 | } 38 | 39 | /// Configuration of services and characteristics desired peripheral should contain. 40 | public let configuration: Configuration 41 | 42 | /// A device parameter. Should be cached locally in order to pass for every connection after the first one. 43 | /// If passed, every connection should happen much quicker. 44 | public var deviceIdentifier: String? 45 | 46 | /// Name of the peripheral returned from Apple native peripheral class. 47 | public var name: String? { 48 | return peripheral?.name 49 | } 50 | 51 | /// Last received signal strength indicator of the peripheral, in decibels. 52 | public var rssi: NSNumber? 53 | 54 | /// Handler which will be called when device will be disconnected. 55 | public var disconnectionHandler: (() -> Void)? 56 | 57 | internal var advertisementData: [AdvertisementData]? 58 | 59 | /// Private instance of Apple native peripheral class. Used to manage write and read requests. 60 | internal var peripheral: CBPeripheral? { 61 | didSet { 62 | peripheral?.delegate = self 63 | } 64 | } 65 | 66 | /// Private variable for storing reference to write completion callback. 67 | internal var writeHandler: ((TransmissionError?) -> ())? 68 | 69 | /// Private variable for storing reference to read completion callback. 70 | internal var readHandler: ((Data?, TransmissionError?) -> ())? 71 | 72 | /// Private variable for storing reference to read rssi completion callback. 73 | internal var rssiHandler: ((NSNumber?, TransmissionError?) -> ())? 74 | 75 | /// Called after reading data from characteristic. 76 | /// - SeeAlso: `CBPeripheralDelegate` 77 | /// This should be moved to an extension in Swift 5 according to: https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md feature. 78 | public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { 79 | defer { 80 | writeHandler = nil 81 | } 82 | guard let handler = writeHandler else { return } 83 | guard let error = error else { 84 | handler(nil) 85 | return 86 | } 87 | handler(.auxiliaryError(error)) 88 | } 89 | 90 | /// Called in two cases: 91 | /// 1) After performing read request from peripheral. 92 | /// 2) After peripheral updates value for characteristic with notify turned on. 93 | /// - SeeAlso: `CBPeripheralDelegate` 94 | public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 95 | defer { 96 | readHandler = nil 97 | } 98 | // It's assumed that if someone performed a read request, we'll ignore calling a notification for this value. 99 | guard let handler = readHandler, error == nil else { 100 | let wrapped = configuration.characteristic(matching: characteristic) 101 | wrapped?.notifyHandler?(characteristic.value) 102 | return 103 | } 104 | guard let error = error else { 105 | handler(characteristic.value, nil) 106 | return 107 | } 108 | handler(nil, .auxiliaryError(error)) 109 | } 110 | 111 | /// Called after reading RRSI value from peripheral. 112 | /// - SeeAlso: `CBPeripheralDelegate` 113 | public func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { 114 | defer { 115 | rssiHandler = nil 116 | } 117 | 118 | guard let handler = rssiHandler else { return } 119 | guard let error = error else { 120 | rssi = RSSI 121 | handler(RSSI, nil) 122 | return 123 | } 124 | handler(nil, .auxiliaryError(error)) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Framework/Source Files/Model/Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | import CoreBluetooth 8 | 9 | /// Struct wrapping Apple's native CBService class. Used to create Configuration for this framework. 10 | public class Service { 11 | 12 | /// UUID of desired service. 13 | public let uuid: String 14 | 15 | /// A set of characteristics this service should contain. 16 | public let characteristics: [Characteristic] 17 | 18 | /// CBUUID parsed from passed UUID String. 19 | internal let bluetoothUUID: CBUUID 20 | 21 | /// Mutable characteristic used for advertisement. 22 | internal var advertisementService: CBMutableService? 23 | 24 | /// Initializes a new instance of Service. It's failable if passed UUID String is not parseable to UUID standards. 25 | /// 26 | /// - Parameters: 27 | /// - uuid: UUID of desired service, should be parseable to CBUUID in order for the initializer to work. 28 | /// - characteristics: a list of Characteristic desired Service should contain. Please note that this list does not have to be exhaustive and contain all characteristics desired service contains on the peripheral. Pass only ones 29 | /// you wish to use. 30 | /// - Throws: CBUUID.CreationError 31 | /// - SeeAlso: `CBUUID.CreationError` 32 | public init(uuid: String, characteristics: [Characteristic]) throws { 33 | self.bluetoothUUID = try CBUUID(uuidString: uuid) 34 | self.uuid = uuid 35 | self.characteristics = characteristics 36 | } 37 | } 38 | 39 | internal extension Service { 40 | 41 | /// Creates CBMutableService used for advertisement and assigns it to the local variable. 42 | func assignAdvertisementService() -> CBMutableService { 43 | let service = CBMutableService(type: bluetoothUUID, primary: true) 44 | var cbCharacteristics = [CBMutableCharacteristic]() 45 | characteristics.forEach { characteristic in 46 | let cbCharacteristc = CBMutableCharacteristic(type: characteristic.bluetoothUUID, properties: [.read, .write, .notify], value: nil, permissions: [.readable, .writeable]) 47 | cbCharacteristics.append(cbCharacteristc) 48 | characteristic.advertisementCharacteristic = cbCharacteristc 49 | } 50 | service.characteristics = cbCharacteristics 51 | advertisementService = service 52 | return service 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Framework/Source Files/Utilities/DispatchQueueProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2023 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | /// Protocol describing a `DispatchQueue`. 9 | protocol DispatchQueueProtocol: AnyObject { 10 | 11 | /// Submits a work item for execution and returns the results from that item after it finishes executing. 12 | /// - Parameter work: The block that contains the work to perform. 13 | /// - Returns: The return value of the item in the work parameter. 14 | func sync(execute work: () throws -> T) rethrows -> T 15 | 16 | /// Schedules a block asynchronously for execution 17 | /// - Parameters: 18 | /// - flags: Additional attributes to apply when executing the block. For a list of possible values, see `DispatchWorkItemFlags`. 19 | /// - work: The block containing the work to perform. This block has no return value and no parameters. 20 | func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) 21 | } 22 | 23 | extension DispatchQueue: DispatchQueueProtocol { 24 | 25 | /// Convenience version of `DispatchQueue/async(group:qos:flags:execute:)` method. 26 | /// SeeAlso: ``DispatchQueueProtocol/async(flags:execute:)`` 27 | func async(flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void) { 28 | async(group: nil, qos: .unspecified, flags: flags, execute: work) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Framework/Source Files/Utilities/SynchronizedArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2023 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | 8 | /// A thread-safe `Array`, which synchronizes access to its elements. 9 | /// To avoid race condition while performing multiple operations on ``SynchronizedArray`` use ``SynchronizedArray/read(_:)`` and ``SynchronizedArray/write(_:)`` methods. 10 | final class SynchronizedArray { 11 | 12 | /// A queue which manages access to ``SynchronizedArray/storage``. Should be a concurrent queue. 13 | private let queue: DispatchQueueProtocol 14 | /// Internal storage of the ``SynchronizedArray``. 15 | private var storage: [Element] 16 | 17 | /// Initializes new ``SynchronizedArray`` with empty storage. 18 | /// - Parameter queue: A queue which manages access to internal storage. Concurrent queue is recommended. 19 | init( 20 | queue: DispatchQueueProtocol = DispatchQueue(label: "co.netguru.lib.blueswift.SynchronizedArray", attributes: .concurrent) 21 | ) { 22 | self.queue = queue 23 | storage = [] 24 | } 25 | 26 | /// Initializes new ``SynchronizedArray`` with containing provided elements. 27 | convenience init(contentsOf array: [Element]) { 28 | self.init() 29 | storage = array 30 | } 31 | 32 | /// Allows accessing element by its index. 33 | subscript(_ index: Int) -> Element { 34 | get { 35 | read { 36 | $0[index] 37 | } 38 | } 39 | set { 40 | write { 41 | $0[index] = newValue 42 | } 43 | } 44 | } 45 | } 46 | 47 | extension SynchronizedArray { 48 | 49 | /// The number of elements in the array. 50 | var count: Int { 51 | read { $0.count } 52 | } 53 | 54 | /// A Boolean value indicating whether the collection is empty. 55 | var isEmpty: Bool { 56 | read { $0.isEmpty } 57 | } 58 | 59 | /// The first element of the array. 60 | var first: Element? { 61 | read { $0.first } 62 | } 63 | 64 | /// The last element of the collection. 65 | var last: Element? { 66 | read { $0.last } 67 | } 68 | 69 | /// Adds a new element at the end of the array. 70 | /// - Parameter newElement: The element to append to the array. 71 | func append(_ newElement: Element) { 72 | write { 73 | $0.append(newElement) 74 | } 75 | } 76 | 77 | /// Adds a new element at the end of the array. 78 | /// - Parameter newElement: The element to append to the array. 79 | func append(contentsOf newElements: S) where Element == S.Element, S : Sequence { 80 | write { 81 | $0.append(contentsOf: newElements) 82 | } 83 | } 84 | 85 | /// Removes and returns the element at the specified position. 86 | /// - Parameter index: The position of the element to remove. index must be a valid index of the array. 87 | func remove(at index: Int) { 88 | write { 89 | $0.remove(at: index) 90 | } 91 | } 92 | 93 | /// Removes all the elements that satisfy the given predicate. 94 | /// - Parameter shouldBeRemoved: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be removed from the array. 95 | func removeAll(where shouldBeRemoved: @escaping (Element) -> Bool) { 96 | write { 97 | $0.removeAll(where: shouldBeRemoved) 98 | } 99 | } 100 | 101 | /// Removes the element at the specified position, checking if provided position is a valid index of the array. 102 | /// - Parameter index: The position of the element to remove. 103 | func safelyRemove(at index: Int) { 104 | write { 105 | guard index < $0.count, index >= 0 else { return } 106 | $0.remove(at: index) 107 | } 108 | } 109 | 110 | /// Returns the first element of the sequence that satisfies the given predicate. 111 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. 112 | /// - Returns: an element from the array. 113 | func first(where predicate: (Element) -> Bool) -> Element? { 114 | read { 115 | $0.first(where: predicate) 116 | } 117 | } 118 | } 119 | 120 | extension SynchronizedArray { 121 | 122 | /// Use `read` method to execute multiple read-operations on the data. 123 | /// - Parameter block: block of code which takes the storage (`[Element]`) as argument and returns a value. 124 | /// - Returns: result of block of code. 125 | func read(_ block: ([Element]) throws -> T) rethrows -> T { 126 | try queue.sync { 127 | try block(storage) 128 | } 129 | } 130 | /// Use `write` method to execute multiple write-operations on the data. 131 | /// - Parameter block: block of code which takes the storage (`[Element]`) as argument and allows to mutate it. 132 | func write(_ block: @escaping (inout [Element]) -> Void) { 133 | queue.async(flags: .barrier) { 134 | block(&self.storage) 135 | } 136 | } 137 | } 138 | 139 | extension SynchronizedArray: ExpressibleByArrayLiteral { 140 | 141 | /// SeeAlso: ``ExpressibleByArrayLiteral/init(arrayLiteral:)``. 142 | convenience init(arrayLiteral elements: Element...) { 143 | self.init(contentsOf: elements) 144 | } 145 | } 146 | 147 | extension SynchronizedArray: Equatable where Element: Equatable { 148 | 149 | /// SeeAlso: `Equatable/==(lhs:rhs:)`. 150 | static func == (lhs: SynchronizedArray, rhs: SynchronizedArray) -> Bool { 151 | lhs.storage == rhs.storage 152 | } 153 | } 154 | 155 | extension SynchronizedArray: CustomDebugStringConvertible { 156 | 157 | /// SeeAlso: `CustomDebugStringConvertible/debugDescription`. 158 | var debugDescription: String { 159 | storage.debugDescription 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Framework/Supporting Files/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 | FMWK 17 | CFBundleShortVersionString 18 | $(PRODUCT_BUNDLE_VERSION_STRING) 19 | CFBundleVersion 20 | $(PRODUCT_BUNDLE_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Netguru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Title 2 | 3 | 4 | ### Motivation 5 | 6 | 7 | ### Task Description 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "BlueSwift", 6 | platforms: [ .iOS(.v10) ], 7 | products: [ 8 | .library( 9 | name: "BlueSwift", 10 | targets: ["BlueSwift"]), 11 | ], 12 | targets: [ 13 | .target( 14 | name: "BlueSwift", 15 | path: "Framework/Source Files" 16 | ), 17 | .testTarget(name: "BlueSwiftTests", 18 | dependencies: ["BlueSwift"], 19 | path: "Unit Tests" 20 | ), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![](./logo.png) 2 | 3 |
4 | 5 | ![](https://img.shields.io/badge/swift-4.2-orange.svg) 6 | ![](https://img.shields.io/github/release/netguru/BlueSwift.svg) 7 | ![](https://img.shields.io/badge/carthage-compatible-green.svg) 8 | ![](https://img.shields.io/badge/cocoapods-compatible-green.svg) 9 | ![](https://app.bitrise.io/app/23a07b63b3f55f97/status.svg?token=Rt_2gKUavbR8LQ7PVuTbYg&branch=master) 10 | 11 | Easy to use Bluetooth open source library brought to you by Netguru.
12 | 🤟 Probably the easiest way to interact with bluetooth peripherals 🤟 13 | 14 | ## 🤹🏻‍♂️ Features 15 | 16 | - [x] Handles connection with remote peripherals. 17 | - [x] Handles advertising an iPhone as Bluetooth LE peripheral. 18 | - [x] Closure based read/write/notify requests. 19 | - [x] Built in data conversion method with `Command` wrapper. 20 | 21 | ## 📲 Connection: 22 | 23 | Below you can find an easy code sample to connect to the peripheral.
24 | Really thats all that is needed 🍾🍾 25 | 26 | ```swift 27 | let connection = BluetoothConnection.shared 28 | let characteristic = try! Characteristic(uuid: "your_characteristic_uuid", shouldObserveNotification: true) 29 | let service = try! Service(uuid: "your_service_uuid", characteristics: [characteristic]) 30 | let configuration = try! Configuration(services: [service], advertisement: "your_advertising_uuid") 31 | let peripheral = Peripheral(configuration: configuration) 32 | connection.connect(peripheral) { error in 33 | // do awesome stuff 34 | } 35 | ``` 36 | 37 | ## 📡 Advertisement: 38 | 39 | Below you can find a code sample the setup the iPhone to advertise Bluetooth.
40 | That's all it takes to advertise one service containing one characteristic. 41 | 42 | ```swift 43 | let characteristic = try! Characteristic(uuid: "your_characteristic_uuid") 44 | let service = try! Service(uuid: "your_service_uuid", characteristics: [characteristic]) 45 | let configuration = try! Configuration(services: [service], advertisement: "your_service_uuid") 46 | let peripheral = Peripheral(configuration: configuration, advertisementData: [.localName("Test"), .servicesUUIDs("your_service_uuid")]) 47 | advertisement.advertise(peripheral: peripheral) { error in 48 | // oh no, something failed in that case 49 | } 50 | ``` 51 | 52 | ## 📟 📲 Data transfer: 53 | 54 | Of course data transfer is also possible, both for advertising and connection mode! 55 | Below there are some basic examples, for more please see `More usage` section 👇🏻 56 | 57 | ## Connection mode: 58 | 59 | ```swift 60 | let command = Command.utf8String("Hello world") 61 | peripheral.write(command: command, characteristic: someCharacteristic, handler: { error in 62 | // written! 63 | }) 64 | peripheral.read(characteristic, handler: { data, error in 65 | // read! 66 | }) 67 | ``` 68 | 69 | ## Advertisement mode: 70 | 71 | ```swift 72 | let command = Command.int8(3) 73 | advertisement.update(command, characteristic: characteristic) { error in 74 | // data updated! 75 | } 76 | advertisement.writeRequestCallback = { characteristic, data in 77 | // written! 78 | } 79 | ``` 80 | 81 | ## ⚙️ More usage: 82 | 83 | For more advanced usage check out documentation page at: https://netguru.github.io/BlueSwift/.
84 | Also feel free to check example project bundled with this repository! 👩🏼‍🏫 👨🏼‍🏫 85 | It's a complete app that allows connection and sending text messages between two iPhones. 86 | 87 | ## 🛠 Dependency management: 88 | 89 | BlueSwift can be drag'n dropped to the project directory,
90 | but what's more important it's supported by most common dependency management! 91 | 92 | ### ![](https://img.shields.io/badge/cocoapods-compatible-green.svg) 93 | 94 | Just drop the line below to your Podfile: 95 | 96 | `pod 'BlueSwift'` 97 | 98 | (but probably you'd like to pin it to the nearest major release, so `pod 'BlueSwift' , '~> 1.1.6'`) 99 | 100 | ### ![](https://img.shields.io/badge/carthage-compatible-green.svg) 101 | 102 | The same as with Cocoapods, insert the line below to your Cartfile: 103 | 104 | `github 'netguru/BlueSwift'` 105 | 106 | , or including version - `github 'netguru/BlueSwift' ~> 1.1.6` 107 | 108 | ## 📄 License 109 | 110 | (As all cool open source software, it's...)
111 | Licensed under MIT license.
112 | 113 | Also it would be really nice if you could drop us a line about your usage!! 🚀🚀 114 | -------------------------------------------------------------------------------- /Unit Tests/CommandTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import XCTest 7 | @testable import BlueSwift 8 | 9 | class CommandTests: XCTestCase { 10 | 11 | func testHexDataConversion() { 12 | let properShortString = "1A2B" 13 | let properLongString = "0123456789ABCDEF" 14 | let improperString = "12345Z" 15 | 16 | let shortData = try! properShortString.hexDecodedData() 17 | let shortFirst = shortData.subdata(in: 0..<1) 18 | XCTAssertEqual(shortFirst.hexEncodedString, "1A", "Incorrect hex string parse") 19 | let shortSecond = shortData.subdata(in: 1..<2) 20 | XCTAssertEqual(shortSecond.hexEncodedString, "2B", "Incorrect hex string parse") 21 | let longData = try! properLongString.hexDecodedData() 22 | let longFirst = longData.subdata(in: 0..<1) 23 | XCTAssertEqual(longFirst.hexEncodedString, "01", "Incorrect hex string parse") 24 | let longSecond = longData.subdata(in: 3..<4) 25 | XCTAssertEqual(longSecond.hexEncodedString, "67", "Incorrect hex string parse") 26 | let longLast = longData.subdata(in: 7..<8) 27 | XCTAssertEqual(longLast.hexEncodedString, "EF", "Incorrect hex string parse") 28 | 29 | do { 30 | _ = try improperString.hexDecodedData() 31 | XCTFail("Improper string successfull parse should throw an error.") 32 | } 33 | catch let error { 34 | guard let error = error as? Command.ConversionError else { 35 | XCTFail() 36 | return 37 | } 38 | XCTAssertEqual(error, Command.ConversionError.incorrectInputFormat, "Incorrect error created on wrong hex string parse") 39 | } 40 | } 41 | 42 | func testIntegerDataConversion() { 43 | 44 | let eightInt: UInt8 = 3 45 | let sixteenInt: UInt16 = 200 46 | let thirtyTwoInt: UInt32 = 65637 47 | 48 | let eightData = eightInt.decodedData 49 | XCTAssertEqual(eightData.count, 1, "Incorrect length for 8 bit data.") 50 | let sixteenData = sixteenInt.decodedData 51 | XCTAssertEqual(sixteenData.count, 2, "Incorrect length for 16 bit data.") 52 | let thirtyTwoData = thirtyTwoInt.decodedData 53 | XCTAssertEqual(thirtyTwoData.count, 4, "Incorrect length for 32 bit data.") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Unit Tests/ConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import XCTest 7 | import CoreBluetooth.CBUUID 8 | @testable import BlueSwift 9 | 10 | class ConfigurationTests: XCTestCase { 11 | 12 | func testServiceUUIDCreation() { 13 | 14 | let correctUUIDLong = "4983BB4D-EC7A-47DD-88E7-EB48859B083A" 15 | let correctUUIDShort = "1800" 16 | let incorrectUUID = "test" 17 | 18 | let serviceLong = try? Service(uuid: correctUUIDLong, characteristics: []) 19 | let serviceShort = try? Service(uuid: correctUUIDShort, characteristics: []) 20 | 21 | XCTAssertEqual(serviceLong?.bluetoothUUID.uuidString, correctUUIDLong, "Incorrect parse of long UUID's for Service.") 22 | XCTAssertEqual(serviceShort?.bluetoothUUID.uuidString, correctUUIDShort, "Incorrect parse of short UUID's for Service.") 23 | 24 | do { 25 | _ = try Service(uuid: incorrectUUID, characteristics: []) 26 | XCTFail("Improper Service creation should throw an error.") 27 | } catch let error { 28 | guard let error = error as? CBUUID.CreationError else { 29 | XCTFail() 30 | return 31 | } 32 | XCTAssertEqual(error, CBUUID.CreationError.invalidString, "Incorrect error returned when parsing wrong UUID.") 33 | } 34 | } 35 | 36 | func testCharacteristicUUIDCreation() { 37 | 38 | let correctUUIDLong = "9CD82657-17AC-404D-B9D6-CF7D4F0F94A2" 39 | let correctUUIDShort = "2A01" 40 | let incorrectUUID = "2A01B" 41 | 42 | let characteristicLong = try? Characteristic(uuid: correctUUIDLong) 43 | let characteristicShort = try? Characteristic(uuid: correctUUIDShort) 44 | 45 | XCTAssertEqual(characteristicLong?.bluetoothUUID.uuidString, correctUUIDLong, "Incorrect parse of long UUID's for Characteristic.") 46 | XCTAssertEqual(characteristicShort?.bluetoothUUID.uuidString, correctUUIDShort, "Incorrect parse of short UUID's for Characteristic.") 47 | 48 | do { 49 | _ = try Characteristic(uuid: incorrectUUID) 50 | XCTFail("Improper Characteristic creation should throw an error.") 51 | } catch let error { 52 | guard let error = error as? CBUUID.CreationError else { 53 | XCTFail() 54 | return 55 | } 56 | XCTAssertEqual(error, CBUUID.CreationError.invalidString, "Incorrect error returned when parsing wrong UUID.") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Unit Tests/ExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import XCTest 7 | import CoreBluetooth 8 | @testable import BlueSwift 9 | 10 | class ExtensionTests: XCTestCase { 11 | 12 | //MARK: String extension tests 13 | 14 | func testShortUUIDValidation() { 15 | let fourCharactersCorrect = "1800" 16 | let sixCharactersCorrext = "2A0B16" 17 | 18 | let tooLong = "1800000" 19 | let tooShort = "67A" 20 | 21 | let notHexadecimal = "180X" 22 | 23 | XCTAssertTrue(fourCharactersCorrect.isValidShortenedUUID(), "Four character valid uuid validation failed") 24 | XCTAssertTrue(sixCharactersCorrext.isValidShortenedUUID(), "Six character valid uuid validation failed") 25 | XCTAssertFalse(tooLong.isValidShortenedUUID(), "Too long invalid uuid validation passed") 26 | XCTAssertFalse(tooShort.isValidShortenedUUID(), "Too short invalid uuid validation passed") 27 | XCTAssertFalse(notHexadecimal.isValidShortenedUUID(), "Not hexadecimal invalid uuid validation passed") 28 | } 29 | 30 | //MARK: Array extension tests 31 | 32 | func testArrayCommonPart() { 33 | let first = [1, 2, 3, 4, 5] 34 | let second = [4, 5, 6, 7] 35 | 36 | let common = first.matchingElementsWith(second) { $0 == $1 } 37 | 38 | XCTAssertEqual(common.count, 2, "Expected matches count is invalid") 39 | XCTAssertEqual(common.first?.0, 4, "Array first element matching failed") 40 | XCTAssertEqual(common.last?.1, 5, "Array second element matching failed") 41 | } 42 | 43 | func testServiceMatching() { 44 | let services = [try! Service(uuid: "1800", characteristics: []), 45 | try! Service(uuid: "1801", characteristics: [])] 46 | let cbServices = [CBMutableService(type: CBUUID(string: "1800"), primary: false) as CBService, 47 | CBMutableService(type: CBUUID(string: "1802"), primary: false) as CBService] 48 | 49 | let common = services.matchingElementsWith(cbServices) 50 | 51 | XCTAssertEqual(common.count, 1, "Expected matches count is invalid") 52 | XCTAssertEqual(common.first?.0.bluetoothUUID.uuidString, common.first?.1.uuid.uuidString, "Expected uuid's does not match") 53 | } 54 | 55 | func testCharacteristicMatching() { 56 | let services = [try! Characteristic(uuid: "2A0B"), 57 | try! Characteristic(uuid: "1801")] 58 | let cbCharacteristics = [CBMutableCharacteristic(type: CBUUID(string: "1801"), properties: [.read], value: nil, permissions: .readable) as CBCharacteristic, 59 | CBMutableCharacteristic(type: CBUUID(string: "1800"), properties: [.read], value: nil, permissions: .readable) as CBCharacteristic] 60 | 61 | let common = services.matchingElementsWith(cbCharacteristics) 62 | 63 | XCTAssertEqual(common.count, 1, "Expected matches count is invalid") 64 | XCTAssertEqual(common.first?.0.bluetoothUUID.uuidString, common.first?.1.uuid.uuidString, "Expected uuid's does not match") 65 | } 66 | 67 | func testAdvertisementCombine() { 68 | 69 | let advertisementParameters: [AdvertisementData] = [.localName("Test"), .servicesUUIDs("2A01"), .servicesUUIDs("2A02"), .txPower(30)] 70 | let dictionary = advertisementParameters.combined() 71 | 72 | XCTAssertEqual(dictionary[CBAdvertisementDataLocalNameKey] as? String, "Test") 73 | XCTAssertEqual(dictionary[CBAdvertisementDataTxPowerLevelKey] as? Int, 30) 74 | guard let array = dictionary[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] else { 75 | XCTFail() 76 | return 77 | } 78 | XCTAssertEqual(array[0].uuidString, "2A01") 79 | XCTAssertEqual(array[1].uuidString, "2A02") 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /Unit Tests/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 | $(PRODUCT_BUNDLE_VERSION_STRING) 19 | CFBundleVersion 20 | $(PRODUCT_BUNDLE_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Unit Tests/Mocks/MockDispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import Foundation 7 | @testable import BlueSwift 8 | 9 | class MockDispatchQueue: DispatchQueueProtocol { 10 | 11 | func sync(execute work: () throws -> T) rethrows -> T { 12 | try work() 13 | } 14 | 15 | func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) { 16 | work() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Unit Tests/SequenceExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SequenceExtensionTests.swift 3 | // Bluetooth 4 | // 5 | // Created by Filip Zieliński on 08/04/2022. 6 | // Copyright © 2022 Netguru. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreBluetooth 11 | @testable import BlueSwift 12 | 13 | final class SequenceExtensionTests: XCTestCase { 14 | 15 | private let fixtureReference = SimpleClass() 16 | private let fixtureOptionalReference = SimpleClass() 17 | private lazy var fixtureTestClass = TestClass(someReference: fixtureReference, someOptionalReference: fixtureOptionalReference) 18 | 19 | func testFirstSequenceElement_withIdenticalProperty() { 20 | // given: 21 | var sut: [TestClass] = [ 22 | TestClass(someReference: .init(), someOptionalReference: nil), 23 | TestClass(someReference: .init(), someOptionalReference: .init()), 24 | fixtureTestClass, 25 | TestClass(someReference: .init(), someOptionalReference: .init()), 26 | ] 27 | 28 | // then: 29 | testFindingElement_withMatchingIdenticalProperty(sut: sut) 30 | 31 | // given 32 | sut = [ 33 | fixtureTestClass, 34 | TestClass(someReference: .init(), someOptionalReference: .init()), 35 | TestClass(someReference: .init(), someOptionalReference: .init()), 36 | TestClass(someReference: .init(), someOptionalReference: nil) 37 | ] 38 | 39 | // then: 40 | testFindingElement_withMatchingIdenticalProperty(sut: sut) 41 | 42 | // given 43 | sut = [ 44 | TestClass(someReference: .init(), someOptionalReference: .init()), 45 | TestClass(someReference: .init(), someOptionalReference: nil), 46 | TestClass(someReference: .init(), someOptionalReference: .init()), 47 | fixtureTestClass 48 | ] 49 | 50 | // then: 51 | testFindingElement_withMatchingIdenticalProperty(sut: sut) 52 | } 53 | } 54 | 55 | private extension SequenceExtensionTests { 56 | 57 | func testFindingElement_withMatchingIdenticalProperty(sut: [TestClass]) { 58 | XCTAssert(sut.first(where: \.someReference, isIdenticalTo: fixtureReference) === fixtureTestClass, "Should find object with matching identical (`===`) property") 59 | XCTAssert(sut.first(where: \.someOptionalReference, isIdenticalTo: fixtureOptionalReference) === fixtureTestClass, "Should find object with matching identical (`===`) property") 60 | XCTAssertNil(sut.first(where: \.someReference, isIdenticalTo: fixtureOptionalReference), "Should not find any object with matching identical (`===`) property") 61 | XCTAssertNil(sut.first(where: \.someOptionalReference, isIdenticalTo: fixtureReference), "Should not find any object with matching identical (`===`) property") 62 | XCTAssertNil(sut.first(where: \.someReference, isIdenticalTo: .init()), "Should not find any object with matching identical (`===`) property") 63 | XCTAssertNil(sut.first(where: \.someOptionalReference, isIdenticalTo: .init()), "Should not find any object with matching identical (`===`) property") 64 | } 65 | } 66 | 67 | private final class TestClass: Identifiable { 68 | 69 | let someReference: SimpleClass 70 | let someOptionalReference: SimpleClass? 71 | 72 | init(someReference: SimpleClass, someOptionalReference: SimpleClass?) { 73 | self.someReference = someReference 74 | self.someOptionalReference = someOptionalReference 75 | } 76 | } 77 | 78 | private final class SimpleClass {} 79 | -------------------------------------------------------------------------------- /Unit Tests/SynchronizedArrayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Netguru Sp. z o.o. All rights reserved. 3 | // Licensed under the MIT License. 4 | // 5 | 6 | import XCTest 7 | import CoreBluetooth.CBUUID 8 | @testable import BlueSwift 9 | 10 | class SynchronizedArrayTests: XCTestCase { 11 | 12 | func testInitializers() { 13 | // given 14 | var sut = SynchronizedArray() 15 | 16 | // then 17 | XCTAssertTrue(sut.isEmpty) 18 | XCTAssertEqual(sut.count, 0) 19 | 20 | // given 21 | sut = .init(arrayLiteral: 1, 2, 3) 22 | 23 | // then 24 | XCTAssertEqual(sut.count, 3) 25 | XCTAssertEqual(sut[0], 1) 26 | XCTAssertEqual(sut[1], 2) 27 | XCTAssertEqual(sut[2], 3) 28 | 29 | // given 30 | sut = .init(contentsOf: [3, 4]) 31 | 32 | // then 33 | XCTAssertEqual(sut.count, 2) 34 | XCTAssertEqual(sut[0], 3) 35 | XCTAssertEqual(sut[1], 4) 36 | } 37 | 38 | func testSubscript() { 39 | // given 40 | let queue = MockDispatchQueue() 41 | let sut: SynchronizedArray = .init(queue: queue) 42 | sut.append(contentsOf: [1, 2, 3]) 43 | 44 | // then 45 | XCTAssertEqual(sut[0], 1) 46 | XCTAssertEqual(sut[1], 2) 47 | XCTAssertEqual(sut[2], 3) 48 | 49 | // when 50 | sut[0] = 4 51 | sut[1] = 5 52 | sut[2] = 6 53 | 54 | // then 55 | XCTAssertEqual(sut, [4, 5, 6]) 56 | } 57 | 58 | func testFirstAndLast() { 59 | // given 60 | var sut: SynchronizedArray = [] 61 | 62 | // then 63 | XCTAssertNil(sut.first) 64 | XCTAssertNil(sut.last) 65 | 66 | // given 67 | sut = [2] 68 | 69 | // then 70 | XCTAssertEqual(sut.first, 2) 71 | XCTAssertEqual(sut.last, 2) 72 | 73 | // given 74 | sut = [1, 2, 3] 75 | 76 | // then 77 | XCTAssertEqual(sut.first, 1) 78 | XCTAssertEqual(sut.last, 3) 79 | 80 | // when 81 | let element = sut.first(where: { $0 == 2 }) 82 | 83 | // then 84 | XCTAssertEqual(element, 2) 85 | } 86 | 87 | func testEquality() { 88 | // given 89 | var sut1: SynchronizedArray = [] 90 | var sut2: SynchronizedArray = [] 91 | 92 | // then 93 | XCTAssertEqual(sut1, sut2) 94 | 95 | // given 96 | sut1 = ["string"] 97 | sut2 = [] 98 | 99 | // then 100 | XCTAssertNotEqual(sut1, sut2) 101 | 102 | // given 103 | sut1 = ["fixture", "string"] 104 | sut2 = ["fixture", "string"] 105 | 106 | // then 107 | XCTAssertEqual(sut1, sut2) 108 | } 109 | 110 | func testAppend() { 111 | // given 112 | let queue = MockDispatchQueue() 113 | let sut: SynchronizedArray = .init(queue: queue) 114 | 115 | // when 116 | sut.append(1) 117 | 118 | // then 119 | XCTAssert(sut == [1]) 120 | 121 | // when 122 | sut.append(2) 123 | 124 | // then 125 | XCTAssertEqual(sut, [1, 2]) 126 | 127 | // when 128 | sut.append(contentsOf: [3, 4, 5]) 129 | 130 | // then 131 | XCTAssertEqual(sut, [1, 2, 3, 4, 5]) 132 | } 133 | 134 | func testRemove() { 135 | // given 136 | let queue = MockDispatchQueue() 137 | let sut: SynchronizedArray = .init(queue: queue) 138 | 139 | // when 140 | sut.removeAll(where: { _ in true }) 141 | 142 | // then 143 | XCTAssert(sut.isEmpty) 144 | 145 | // given 146 | sut.append(contentsOf: [1, 2, 3]) 147 | 148 | // when 149 | sut.removeAll(where: { _ in false }) 150 | 151 | // then 152 | XCTAssertEqual(sut, [1, 2, 3]) 153 | 154 | // when 155 | sut.removeAll(where: { $0 == 2 }) 156 | 157 | // then 158 | XCTAssertEqual(sut, [1, 3]) 159 | 160 | // when 161 | sut.removeAll(where: { _ in true }) 162 | 163 | // then 164 | XCTAssertEqual(sut, []) 165 | 166 | // given 167 | sut.append(contentsOf: [1, 2, 3, 4, 5]) 168 | 169 | // when 170 | sut.safelyRemove(at: 5) 171 | 172 | // then 173 | XCTAssertEqual(sut, [1, 2, 3, 4, 5]) 174 | 175 | // when 176 | sut.safelyRemove(at: 4) 177 | 178 | // then 179 | XCTAssertEqual(sut, [1, 2, 3, 4]) 180 | 181 | // when 182 | sut.remove(at: 3) 183 | 184 | // then 185 | XCTAssertEqual(sut, [1, 2, 3]) 186 | 187 | // when 188 | sut.remove(at: 0) 189 | 190 | // then 191 | XCTAssertEqual(sut, [2, 3]) 192 | } 193 | 194 | func testConcurrentAppend() { 195 | let sut = SynchronizedArray() 196 | let iterations = 1000 197 | 198 | // Regular Swift `Array` would crash here (`EXC_BAD_ACCESS`): 199 | DispatchQueue.concurrentPerform(iterations: iterations) { index in 200 | sut.append(index) 201 | } 202 | 203 | XCTAssertEqual(sut.count, iterations) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | # 2 | # bitrise.yml 3 | # Copyright © 2017 Netguru Sp. z o.o. All rights reserved. 4 | # 5 | # This `bitrise.yml` variant contains the default configuration to be used in 6 | # an iOS app repository. You can strip the comments in your `bitrise.yml` if 7 | # you want. 8 | # 9 | # Scripts in the following `bitrise.yml` file make use of the following 10 | # environment variables that should be set up as secret: 11 | # 12 | # Also, those scripts use the following environment variables that are declared 13 | # publicly in the following file: 14 | # 15 | # - `XCODEBUILD_PROJECT`: A path to a project or a workspace Xcode file. 16 | # 17 | # - `XCODEBUILD_SCHEME`: A name of the scheme to be built. 18 | # 19 | # - `XCODEBUILD_OPTIONS`: An additional build settings passed straight to each 20 | # `xcodebuild` invocation. 21 | # 22 | 23 | # CLI metadata. 24 | # 25 | # This metadata is required to always be in `bitrise.yml`. The `format_version` 26 | # setting will be occasionally bumped. 27 | 28 | format_version: 1.3.1 29 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 30 | 31 | # Workflow trigger map. 32 | # 33 | # The following trigger map triggers a build only for pull requests against 34 | # `develop` and `master` branches (from forks as well) and pushes to `develop` 35 | # and `master` branches. 36 | # 37 | # More on trigger maps: http://devcenter.bitrise.io/webhooks/trigger-map 38 | 39 | trigger_map: 40 | 41 | - push_branch: develop 42 | workflow: build-pull-request 43 | 44 | - push_branch: master 45 | workflow: build-pull-request 46 | 47 | - pull_request_target_branch: develop 48 | workflow: build-pull-request 49 | 50 | - pull_request_target_branch: master 51 | workflow: build-pull-request 52 | 53 | # Environment configuration. 54 | # 55 | # This list contains the default environment variables shared between workflows. 56 | 57 | app: 58 | envs: 59 | - XCODEBUILD_PROJECT: ./Bluetooth.xcodeproj 60 | - XCODEBUILD_OPTIONS: _BUILD_NUMBER=$BITRISE_BUILD_NUMBER 61 | 62 | # Workflow declarations. 63 | # 64 | # This list contains workflows used in the above trigger map. 65 | 66 | workflows: 67 | 68 | build-pull-request: 69 | envs: 70 | - XCODEBUILD_SCHEME: BlueSwift-iOS 71 | before_run: 72 | - test-xcode 73 | 74 | # Bootstrap code signing by installing certificates and provisioning profiles. 75 | 76 | bootstrap-code-signing: 77 | steps: 78 | - certificate-and-profile-installer: {} 79 | 80 | # Build and test an app using `xcodebuild` command. 81 | 82 | test-xcode: 83 | steps: 84 | - xcode-test: 85 | inputs: 86 | - project_path: $XCODEBUILD_PROJECT 87 | - scheme: $XCODEBUILD_SCHEME 88 | - xcodebuild_test_options: $XCODEBUILD_OPTIONS 89 | - simulator_device: iPhone 12 90 | 91 | # Deploy build artifacts to bitrise.io. 92 | 93 | deploy-bitriseio: 94 | steps: 95 | - deploy-to-bitrise-io: 96 | inputs: 97 | - notify_user_groups: none 98 | - is_enable_public_page: false 99 | 100 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/advertisementData.md: -------------------------------------------------------------------------------- 1 | ## AdverisementData 2 | 3 | ## Overview 4 | 5 | It's a convenient enum to deal with values put in advertisement header when acting as a peripheral. Used when initializing [Peripheral](./peripheral.md) in an `Advertisable` generic mode. It's intended to be a wrapper around CoreBluetooth default statics such as `CBAdvertisementDataServiceUUIDsKey` which makes them hard to remember. 6 | 7 | ## Topics 8 | 9 | ```swift 10 | case localName(String) 11 | ```swift 12 | 13 | ```swift 14 | case servicesUUIDs(String) 15 | ```swift 16 | 17 | ```swift 18 | case serviceData(Data) 19 | ```swift 20 | 21 | ```swift 22 | case txPower(Int) 23 | ```swift 24 | 25 | ```swift 26 | case manufacturersData(String) 27 | ```swift 28 | 29 | ```swift 30 | case custom(String, Any) 31 | ```swift 32 | -------------------------------------------------------------------------------- /docs/bluetoothAdvertisement.md: -------------------------------------------------------------------------------- 1 | ## BleutoothAdvertisement 2 | 3 | 🤖 Work in progress, please check back in a while 🤖 -------------------------------------------------------------------------------- /docs/bluetoothConnection.md: -------------------------------------------------------------------------------- 1 | ## BluetoothConnection 2 | 3 | ## Overview 4 | 5 | It's a class responsible for management of peripheral connections. It's a singleton by design as it's recommended to use only one instance of `CBCentralManager` per application. One instance of that class can be also responsible for connecting multiple peripherals. Although you're welcome to create your own instance. 6 | It uses [Peripheral](./peripheral.md) class for connection. 7 | 8 | ## Topics 9 | 10 | ```swift 11 | static let shared = BluetoothConnection() 12 | ``` 13 | 14 | A singleton instance of the class. Motivation to use it is carried by the fact that it's recommended to use only one instance of `CBCentralManager` per application. 15 | 16 | ```swift 17 | var advertisementValidationHandler: ((Peripheral, String, [String: Any]) -> (Bool))? 18 | ``` 19 | 20 | An optional closure, nil by default. If set you can setup custom filtering of scanned peripherals basing on their advertising packets. 21 | Advertising is passed in `[String: Any]` dictionary. This data is sent along with a string representing a CoreBluetooth identifier of the device(unique and not changing per each device) and corresponding peripheral instance. 22 | If false is returned from this method, no attempt to connect a given peripheral will we attempted. 23 | 24 | ```swift 25 | var peripheralConnectionCancelledHandler: ((Peripheral, CBPeripheral) -> Void)? 26 | ``` 27 | 28 | An optional closure, nil by default - peripheral connection cancelled handler. Called when disconnecting a peripheral using `disconnect(_:)` is completed. 29 | Contains matched peripheral and native peripheral from CoreBluetooth. 30 | 31 | ```swift 32 | func connect(_ peripheral: Peripheral, handler: ((ConnectionError?) -> ())?) 33 | ``` 34 | 35 | Main method responsible for connecting a peripheral. After configuring a proper [Peripheral](./peripheral.md) class it can be passed here to be connected. After it's connected or some error is raised, the `handler` closure is called. If error is nil, you can assume connection went well. 36 | Possible errors are raised when Bluetooth is unavailable or turned off, maximum connected devices limit is exceeded(the limit is 8 devices) or that device is already connected. 37 | 38 | ```swift 39 | func disconnect(_ peripheral: Peripheral) throws 40 | ``` 41 | 42 | Disconnects a given device. It can throw only in case the device is not already connected so the error can be safely ignored in most cases. 43 | -------------------------------------------------------------------------------- /docs/characteristic.md: -------------------------------------------------------------------------------- 1 | ## Characteristic 2 | 3 | ## Overview 4 | 5 | 6 | 7 | ## Topics 8 | 9 | ```swift 10 | public let uuid: String 11 | ``` 12 | 13 | ```swift 14 | let isObservingValue: Bool 15 | ``` 16 | 17 | ```swift 18 | var notifyHandler: ((Data?) -> ())? 19 | ``` 20 | 21 | ```swift 22 | init(uuid: String, shouldObserveNotification: Bool = false) throws 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/command.md: -------------------------------------------------------------------------------- 1 | ## Command 2 | 3 | 🤖 Work in progress, please check back in a while 🤖 -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | 🤖 Work in progress, please check back in a while 🤖 -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Motivation: 2 | 3 | For the past previous years I've worked with several apps related to Bluetooth LE technology. 4 | At the start of every project I've came to a few conclusions: 5 | - CoreBluetooh delegate based API is horrible and really full of boilerplate 6 | - I had to write many small MVP projects(eg. for testing purposes) that made point 1 even more horrible 7 | 8 | That's pretty much everything that motivated me to create a library that will handle connection and data tranfer in just a couple of lines. 9 | I've also felt that it should be handled by closures instead of delegation for easier management. 10 | Below you'll find a bit more thoroughly explained examples along with small documentation piece for every public class used in this project. 11 | Let's go with connection first. 12 | 13 | # Connection: 14 | 15 | Starting with some code to be described in detail later: 16 | 17 | ```swift 18 | let connection = BluetoothConnection.shared 19 | let characteristic = try! Characteristic(uuid: "your_characteristic_uuid", shouldObserveNotification: true) 20 | let service = try! Service(uuid: "your_service_uuid", characteristics: [characteristic]) 21 | let configuration = try! Configuration(services: [service], advertisement: "your_advertising_uuid") 22 | let peripheral = Peripheral(configuration: configuration) 23 | connection.connect(peripheral) { _ in 24 | // do sth 25 | } 26 | ``` 27 | 28 | the idea behind above lines is to create a specification of the expected device with a configuration file. 29 | You should create an instance of `Characteristic` or `Service` classes for each existing ones expected in a connecting device. 30 | Only if all specified characteristics and services are found on device, it's connected with no error. 31 | Please note that you don't have to specify everything contained in the device, just the one's you'll use. 32 | After that a peripheral object is created with a passed configuration, it should be persisted by some instance variable as it will be used to manage connection. 33 | If you wish to ceonnect multiple devices, create multiple peripheral. 34 | The library contains a built in singleton instance of `BluetoothConnection`, you can easily use your own instance, but creating it will create a separate `CBCentralManager` instance, and according to Apple documentation, creating more than one instance is not recommended. 35 | 36 | ```swift 37 | let command = Command.utf8String("Hello world") 38 | peripheral.write(command: command, characteristic: someCharacteristic, handler: { error in 39 | // do sth 40 | }) 41 | ``` 42 | 43 | The library has a built in enum for simple conversions. It may sound easy but it's really convenient :) 44 | Using a simple enum you can create input data of cerrect bit size from integers, convert custom strings to hexadecimal and, if needed, pass data created on your own. 45 | This input data should be passed by `write` method on a peripheral that was previously used to connect. Also characteristic should be passed, this also should be the one used to create configuration for this peripheral instance. 46 | If nil error is returned in the closure, write request succeed. 47 | 48 | ```swift 49 | peripheral.read(characteristic, handler: { data, error in 50 | // do sth 51 | }) 52 | ``` 53 | 54 | Reading values from characteristics is actually pretty similar to writing them, you should grab on the `Peripheral` object instance used for connection, fetch desired characteristic(should also be the one used for creating configuration). 55 | As the result is asynchronous, it's returned by closure - if error there is nil, reading scceed and Data object can be read. 56 | 57 | ```swift 58 | characteristic.notifyHandler = { data in 59 | // do sth 60 | } 61 | ``` 62 | 63 | Do you maybe remember a moment when you had to subscribe to a characteristic and wait for value updates? With `CoreBluetooth` alone this was hell, right? 64 | Here you have a convenient closure in every `Characteristic` object instance. It will be called each time a value changes there. The only prerequisite is to call: 65 | `shouldObserveNotification: true)` in your characteristic initializer, only this way a proper variable will be set after connection and observation will be visible. 66 | 67 | # Advertisement: 68 | 69 | ```swift 70 | let characteristic = try! Characteristic(uuid: "your_characteristic_uuid") 71 | let service = try! Service(uuid: "your_service_uuid", characteristics: [characteristic]) 72 | let configuration = try! Configuration(services: [service], advertisement: "your_service_uuid") 73 | let peripheral = Peripheral(configuration: configuration, advertisementData: [.localName("Test"), .servicesUUIDs("your_service_uuid")]) 74 | advertisement.advertise(peripheral: peripheral) { _ in 75 | // handle possible error 76 | } 77 | ``` 78 | 79 | Advertisement setup is actually not that much different than connection. The main idea of creating a configuration that will specify all services and characteristics of the device is kept, only details varies. 80 | 81 | ```swift 82 | let command = Command.int8(3) 83 | advertisement.update(command, characteristic: characteristic) { error in 84 | // notified subscribed centrals 85 | } 86 | ``` 87 | 88 | Above method uses `Command` enum to create a `Data` object that will be updated on a given characteristic. It wil autmoatically update the value on each central that is currently subscribed to that characteristic. 89 | 90 | ```swift 91 | advertisement.writeRequestCallback = { characteristic, data in 92 | // handle write request 93 | } 94 | ``` 95 | 96 | If you wish to handle write requests from connected centrals, assign custom value to that closure, each write attempt will be called there with associated data and characteristic. 97 | 98 | ```swift 99 | advertisement.readRequestCallback = { characteristic -> Data in 100 | // respond to read request 101 | } 102 | ``` 103 | 104 | If you wish to handle read requests from connected centrals, assign custom value to that closure, each write attempt will be called there with associated characteristic. You should return any data that should be responded. 105 | 106 | # Public classes documentation: 107 | 108 | [BluetoothConnection](./bluetoothConnection.md) 109 | 110 | [Service](./service.md) 111 | 112 | [Characteristic](./characteristic.md) 113 | 114 | [Configuration](./configuration.md) 115 | 116 | [Peripheral](./peripheral.md) 117 | 118 | [Command](./command.md) 119 | 120 | [BluetoothAdvertisement](./bluetoothAdvertisement.md) 121 | 122 | [AdvertisementData](./advertisementData.md) 123 | -------------------------------------------------------------------------------- /docs/peripheral.md: -------------------------------------------------------------------------------- 1 | ## Peripheral 2 | 3 | 🤖 Work in progress, please check back in a while 🤖 -------------------------------------------------------------------------------- /docs/service.md: -------------------------------------------------------------------------------- 1 | ## Service 2 | 3 | ## Overview 4 | 5 | 6 | 7 | ## Topics 8 | 9 | ```swift 10 | let uuid: String 11 | ``` 12 | 13 | ```swift 14 | let characteristics: [Characteristic] 15 | ``` 16 | 17 | ```swift 18 | init(uuid: String, characteristics: [Characteristic]) throws 19 | ``` 20 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/BlueSwift/dacfbd87b80ca2593ab2c402c551eea3c8a56821/logo.png --------------------------------------------------------------------------------