I have setup my app to automatically connect to bluetooth when the app is launched, but I wanted a force reconnect button to be used if connection is dropped. My button uses the same code as the initial search and connect that is used under viewDidLoad().
YES I know about the auto reconnect feature and it is enabled, but this is how I want it.
#IBAction func BTButtonPushed(_ sender: Any) {
bluejay.start() //Call Bluejay
bluejay.scan( //Begin and format BLE scanning
duration: 10, serviceIdentifiers: [self.serviceID],
discovery: { [weak self] (discovery, discoveries) -> ScanAction in
guard let weakSelf = self else {
return .stop
}
if (discovery.peripheralIdentifier.name == self!.deviceName) {
self!.pI = discovery.peripheralIdentifier
return .stop
}
weakSelf.discoveries = discoveries
return .continue
},
stopped: { (discoveries, error) in
//Once the correct device is found, connect\\
self.bluejay.connect(self.pI) {
result in
switch result {
case .success:
self.isConnected = true
self.BTButtonView.setImage(UIImage(named: "Bluetooth-Logo-on"), for: UIControl.State.normal) //Set BT logo to connected
case .failure(let error):
debugPrint(error)
self.isConnected = false
self.BTButtonView.setImage(UIImage(named: "Bluetooth-Logo-off"), for: UIControl.State.normal) //Set BT logo to connected
}
}
})
}
The button will disconnect bluetooth if pressed, but will not turn it back on, for that I have to quit the app and relaunch. Honestly, I do not know why it will disconnect from bluetooth when it is pressed as I dont have any code written to disconnect.
Well I figured it out, and thought I would post if anybody else ran into this problem. I did not have a command to disconnect the bluetooth module, which caused errors for reconnection.
I have since added a branch of logic to determine if the button should be on or off by oscillating a Bool value started by initiating a global variable of BTButton to true.
#IBAction func BTButtonPushed(_ sender: Any) {
if (BTButton) {
bluejay.disconnect(immediate: true)
self.BTButtonView.setImage(UIImage(named: "Bluetooth-Logo-off"), for: UIControl.State.normal) //Set BT logo to connected
BTButton = false
}
else {
bluejay.start() //Call Bluejay
bluejay.scan( //Begin and format BLE scanning
duration: 10, serviceIdentifiers: [self.serviceID],
discovery: { [weak self] (discovery, discoveries) -> ScanAction in
guard let weakSelf = self else {
return .stop
}
if (discovery.peripheralIdentifier.name == self!.deviceName) {
self!.pI = discovery.peripheralIdentifier
return .stop
}
weakSelf.discoveries = discoveries //Add each discovery to discoveries
return .continue
},
stopped: { (discoveries, error) in
//Once the correct device is found, connect\\
self.bluejay.connect(self.pI) {
result in
switch result {
case .success:
self.isConnected = true
self.BTButtonView.setImage(UIImage(named: "Bluetooth-Logo-on"), for: UIControl.State.normal) //Set BT logo to connected
case .failure(let error):
debugPrint(error)
self.isConnected = false
self.BTButtonView.setImage(UIImage(named: "Bluetooth-Logo-off"), for: UIControl.State.normal) //Set BT logo to connected
}
}
})
BTButton = true
}
}
Related
My current program is about mostly TableViews and CollectionViews that fetches data from an API, The problem is that the data is fetched only when the user goes to the current tab which the TableView is present and the data fetches from the moment the user went to the tab and needs to wait till the data come because when user logs-in a token is shared with other classes and used later to fill them.
I don't know how can I make the data filled automatically after the user is logged-in not when the user goes to the specific tab and wait for them to load, Basically make a request when the user loggs-in so he doesn't have to go to the tabs and wait 5+ seconds
signInViewController:
#IBAction func signInSegueToDashboard(_ sender: Any) {
activityLoaderSignIn.startAnimating()
APICallerPOST.shared.signInToAccount(username: emailFieldSignIn.text!, password: passwordFieldSignIn.text!) { [self] (result, error) in
if result?.StatusCode != 0 {
DispatchQueue.main.async {
activityLoaderSignIn.stopAnimating()
}
}
switch result?.StatusCode {
case 0:
APICallerGET.shared.token = result?.Result.token
assignDataFromSignIn.DefaultsKeys.keyOne = (result?.Result.completeName)!
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
guard let mainTabBarController = self.storyboard?.instantiateViewController(withIdentifier: "mainTabBarController")
else {
return
}
mainTabBarController.modalPresentationStyle = .custom
self.present(mainTabBarController, animated: true, completion: nil)
}
case 1:
DispatchQueue.main.async {
}
case 2:
DispatchQueue.main.async {
}
default:
break
}
}
}
invoicesViewController:
// the function which is used at viewDidLoad at a TableView class
func fetchAndReloadData(){
APICallerGET.shared.propertiesOfAccount(for: APICallerGET.shared.token!) { [self] (result, error) in
switch result?.StatusCode {
case 0:
self.notificationsNew = result!
self.notifications = self.notificationsNew
DispatchQueue.main.async {
self.invoicesTableView.reloadData()
activityLoaderInvoices.stopAnimating()
}
case 1:
print("error")
default:
break
}
}
}
Now if there is a way to load this function of the invoicesViewController straight when the user signs-in so he doesn't have to wait till the data loads when he went to the tab with the TableView.
I've made connection checker according to this question How to check internet connection in alamofire?
import Foundation
import Alamofire
class Connectivity {
class var isConnectedToInternet:Bool {
return NetworkReachabilityManager()?.isReachable ?? false
}
}
And I'm checking connection before I make request like this
func fetchPhotos() {
if Connectivity.isConnectedToInternet{
newOrPopularChooser(self.page)
.observe(on: MainScheduler.instance)
.subscribe{ (response) in
self.photos.append(contentsOf: response.data)
self.page += 1
if self.page == response.countOfPages {
self.isEndReached = true
}
self.view?.onFetchPhotoSuccess()
} onError: { error in
self.view?.onFetchPhotoFailure(error: error.localizedDescription)
}.disposed(by: disposeBag)
} else {
self.view?.onFetchPhotoFailure(error: "No Internet connection")
}
}
And here is the code of fetch photo success and failure
func onFetchPhotoSuccess() {
self.refreshControl.endRefreshing()
footerView.stopAnimating()
self.isLoadingList = false
self.collectionView.reloadData()
if !(self.presenter?.photos.isEmpty)! {
self.collectionView!.collectionViewLayout.invalidateLayout()
self.collectionView!.layoutSubviews()
self.collectionView.backgroundView = nil
}
}
func onFetchPhotoFailure(error: String) {
print("View receives the response from Presenter with error: \(error)")
let storyboard = UIStoryboard(name: "NoInternetConnectionView", bundle: .main)
let controller = storyboard.instantiateViewController(identifier: "NoInternetConnectionVC")
self.presenter?.photos = []
self.collectionView.reloadData()
collectionView.backgroundView = controller.view
self.refreshControl.endRefreshing()
footerView.stopAnimating()
}
So if I have internet on I see my collection but when I'm turning my internet off and then turning it back and reload my collection view I'm getting empty collectionView. No no internet connection background just empty background like this
Do not do this. Apple has long told us that checking for connectivity before issuing a network request is an anti pattern. This is for many reasons, but the biggest is the fact that the reachability check may be inaccurate or take inordinate time to produce an answer. It's far better to make the network request and handle any failure that may occur.
I have a login view controller that user Almofire library to get the response. I do the unit test on that controller but the test always fail. I think because take time to response.
My test case:
override func setUp() {
super.setUp()
continueAfterFailure = false
let vc = UIStoryboard(name: "Main", bundle: nil)
controllerUnderTest = vc.instantiateViewController(withIdentifier: "LoginVC") as! LoginViewController
controllerUnderTest.loadView()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
controllerUnderTest = nil
super.tearDown()
}
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I try to use:
waitForExpectations(timeout: 60, handler: nil)
But I got this error:
caught "NSInternalInconsistencyException"
almofire function in login presenter :
func sendRequest(withParameters parameters: [String : String]) {
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
debugPrint("new line : \(response)" )
switch response.result {
case .success(let value):
let userJSON = JSON(value)
self.readResponse(data: userJSON)
case .failure(let error):
print("Error \(String(describing: error))")
self.delegate.showMessage("* Connection issue ")
}
self.delegate.removeLoadingScreen()
//firebase log in
Auth.auth().signIn(withEmail: parameters["email"]!, password: parameters["pass"]!) { [weak self] user, error in
//guard let strongSelf = self else { return }
if(user != nil){
print("login with firebase")
}
else{
print("eroor in somthing")
}
if(error != nil){
print("idon now")
}
// ...
}
}
}
func readResponse(data: JSON) {
switch data["error"].stringValue {
case "true":
self.delegate.showMessage("* Invalid user name or password")
case "false":
if data["state"].stringValue=="0" {
self.delegate.showMessage("logged in successfully")
}else {
self.delegate.showMessage("* Inactive account")
}
default:
self.delegate.showMessage("* Connection issue")
}
}
How can I solve this problem? :(
Hi #Raghad ak, welcome to Stack Overflow 👋.
Your guess about the passage of time preventing the test to succeed is correct.
Networking code is asynchronous. After the test calls .sendActions(for: .touchUpInside) on your login button it moves to the next line, without giving the callback a chance to run.
Like #ajeferson's answer suggests, in the long run I'd recommend placing your Alamofire calls behind a service class or just a protocol, so that you can replace them with a double in the tests.
Unless you are writing integration tests in which you'd be testing the behaviour of your system in the real world, hitting the network can do you more harm than good. This post goes more into details about why that's the case.
Having said all that, here's a quick way to get your test to pass. Basically, you need to find a way to have the test wait for your asynchronous code to complete, and you can do it with a refined asynchronous expectation.
In your test you can do this:
expectation(
for: NSPredicate(
block: { input, _ -> Bool in
guard let label = input as? UILabel else { return false }
return label.text == "logged in successfully"
}
),
evaluatedWith: controllerUnderTest.lblValidationMessage,
handler: .none
)
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
waitForExpectations(timeout: 10, handler: nil)
That expectation will run the NSPredicate on a loop, and fulfill only when the predicate returns true.
You have to somehow signal to your tests that are safe to proceed (i.e. expectation is fulfilled). The ideal approach would be decouple that Alamofire code and mock its behavior when testing. But just to answer your question, you might want to do the following.
In your view controller:
func sendRequest(withParameters parameters: [String : String], completionHandler: (() -> Void)?) {
...
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
...
// Put this wherever appropriate inside the responseJSON closure
completionHandler?()
}
}
Then in your tests:
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
let expectation = self.expectation(description: "logged in successfully)
waitForExpectations(timeout: 60, handler: nil)
controllerUnderTest.sendRequest(withParameters: [:]) {
expectation.fulfill()
}
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I know you have some intermediate functions between the button click and calling the sendRequest function, but this is just for you to get the idea. Hope it helps!
I am following instructions here, I've put together this test project to handle interruptions to audio play. Specifically, I'm using the alarm from the default iphone clock app as interruption. It appears that the interruption handler is getting called but is not getting past the let = interruptionType line as "wrong type" showed up twice.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player = AVAudioPlayer()
let audioPath = NSBundle.mainBundle().pathForResource("rachmaninov-romance-sixhands-alianello", ofType: "mp3")!
func handleInterruption(notification: NSNotification) {
guard let interruptionType = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? AVAudioSessionInterruptionType else { print("wrong type"); return }
switch interruptionType {
case .Began:
print("began")
// player is paused and session is inactive. need to update UI)
player.pause()
print("audio paused")
default:
print("ended")
/**/
if let option = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? AVAudioSessionInterruptionOptions where option == .ShouldResume {
// ok to resume playing, re activate session and resume playing
// need to update UI
player.play()
print("audio resumed")
}
/**/
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
do {
try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: audioPath))
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
//player.delegate = player
} catch {
// process error here
}
// enable play in background https://stackoverflow.com/a/30280699/1827488 but this audio still gets interrupted by alerts
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
// add observer to handle audio interruptions
// using 'object: nil' does not have a noticeable effect
let theSession = AVAudioSession.sharedInstance()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.handleInterruption(_:)), name: AVAudioSessionInterruptionNotification, object: theSession)
// start playing audio
player.play()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Furthermore, following an idea here, I have modified the handler to
func handleInterruption(notification: NSNotification) {
//guard let interruptionType = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? AVAudioSessionInterruptionType else { print("wrong type"); return }
if notification.name != AVAudioSessionInterruptionNotification
|| notification.userInfo == nil{
return
}
var info = notification.userInfo!
var intValue: UInt = 0
(info[AVAudioSessionInterruptionTypeKey] as! NSValue).getValue(&intValue)
if let interruptionType = AVAudioSessionInterruptionType(rawValue: intValue) {
switch interruptionType {
case .Began:
print("began")
// player is paused and session is inactive. need to update UI)
player.pause()
print("audio paused")
default:
print("ended")
/** /
if let option = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? AVAudioSessionInterruptionOptions where option == .ShouldResume {
// ok to resume playing, re activate session and resume playing
// need to update UI
player.play()
print("audio resumed")
}
/ **/
player.play()
print("audio resumed")
}
}
}
Results are that all of "began", "audio paused", "ended" and "audio resumed" show up in console but audio play is not actually resumed.
Note: I moved the player.play() outside of the commented out where option == .ShouldResume if statement because that if condition is not true when the .Ended interruption occurs.
(Posted on behalf of the question author, after it was posted in the question).
Solution found! Following discussion here, inserted this in viewDidLoad()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: AVAudioSessionCategoryOptions.MixWithOthers)
} catch {
}
After clicking "ok" on the alarm interruption, the audio play continued. Unlike previously noted, the solution does NOT require an interruption handler (which #Leo Dabus has since removed).
However if you are using an interruption handler, .play() must NOT be invoked within handleInterruption() as doing so does NOT guarantee play to resume & seems to prevent audioPlayerEndInterruption() to be called (see docs). Instead .play() must be invoked within audioPlayerEndInterruption() (any of its 3 versions) to guarantee resumption.
Furthermore, AVAudioSession must be give option .MixWithOthers noted by #Simon Newstead if you want your app to resume play after interruption when your app is in the background. It seems that if a user wants the app to continue playing when it goes into the background, it is logical to assume the user also wants the app to resume playing after an interruption while the app is in the background. Indeed that is the behaviour exhibited by the Apple Music app.
#rockhammers suggestion worked for me. Here
before class
let theSession = AVAudioSession.sharedInstance()
in viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleInterruption(notification:)), name: NSNotification.Name.AVAudioSessionInterruption, object: theSession)
And then the Function
func handleInterruption(notification: NSNotification) {
print("handleInterruption")
guard let value = (notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? NSNumber)?.uintValue,
let interruptionType = AVAudioSessionInterruptionType(rawValue: value)
else {
print("notification.userInfo?[AVAudioSessionInterruptionTypeKey]", notification.userInfo?[AVAudioSessionInterruptionTypeKey])
return }
switch interruptionType {
case .began:
print("began")
vox.pause()
music.pause()
print("audioPlayer.playing", vox.isPlaying)
/**/
do {
try theSession.setActive(false)
print("AVAudioSession is inactive")
} catch let error as NSError {
print(error.localizedDescription)
}
pause()
default :
print("ended")
if let optionValue = (notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? NSNumber)?.uintValue, AVAudioSessionInterruptionOptions(rawValue: optionValue) == .shouldResume {
print("should resume")
// ok to resume playing, re activate session and resume playing
/**/
do {
try theSession.setActive(true)
print("AVAudioSession is Active again")
vox.play()
music.play()
} catch let error as NSError {
print(error.localizedDescription)
}
play()
}
}
}
some reasons interruptionNotification is not working correctly on iOS 12.x So I added silenceSecondaryAudioHintNotification
With alarm notification incoming, you can try to use silenceSecondaryAudioHintNotification.
#objc func handleSecondaryAudioSilence(notification: NSNotification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt,
let type = AVAudioSession.SilenceSecondaryAudioHintType(rawValue: typeValue) else {
return
}
if type == .end {
// Other app audio stopped playing - restart secondary audio.
reconnectAVPlayer()
}
}
im working on an iphone app for iOS 8.1 that works with core audio to generate frequencies and adjust intensities. In the view controller that i generate the frequencies i need to control if the headphones are plugged out in some moment, i'm already controlling if headphones are connected before proceed to my frequencies generator view with the following function:
- (BOOL)isHeadsetPluggedIn {
AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
for (AVAudioSessionPortDescription* desc in [route outputs]) {
if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones])
return YES;
}
return NO;
}
this function is in C because im working with core-audio to generate the frequencies, but in the view controllers im working with swift so a need a way to implement a listener to detect the headphones plug-out event and return to the user to the previous view, i don't know if i can use my function isHeadsetPluggedin() with an event listener or i should make a new one.
In my MenuViewController i control if the headphones are plugged in using the following function:
func isHeadsetPluggedIn() -> Bool {
return freqController.isHeadsetPluggedIn();
}
In Swift 4
func activateHeadPhonesStatus(){
NotificationCenter.default.addObserver(self, selector: #selector(audioRouteChangeListener(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
}
#objc func audioRouteChangeListener(_ notification:Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
headphonesConnected = true
print("headphone plugged in")
break
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
headphonesConnected = false
print("headphone pulled out")
break
}
}
default: ()
}
}
You can track the route changes by observing AVAudioSessionRouteChangeNotification notification.
//Observe for route changing notification
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];
-(void)handleRouteChange:(NSNotification *)notif
{
NSDictionary *dict = notif.userInfo;
AVAudioSessionRouteDescription *routeDesc = dict[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *prevPort = [routeDesc.outputs objectAtIndex:0];
if ([prevPort.portType isEqualToString:AVAudioSessionPortHeadphones]) {
//Head phone removed
}
}
This article worked for me. There is also a GitHub repo with solution. If you don't want to read, here is my code:
Put this in your INIT method:
self.session = AVAudioSession.sharedInstance()
let currentRoute = self.session.currentRoute
if currentRoute.outputs.count != 0 {
for description in currentRoute.outputs {
if description.portType == AVAudioSessionPortHeadphones {
print("headphone plugged in")
} else {
print("headphone pulled out")
}
}
} else {
print("requires connection to device")
}
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(YOUR_VIEW_CONTROLLER_OR_VIEW.audioRouteChangeListener(_:)),
name: AVAudioSessionRouteChangeNotification,
object: nil)
And put this anywhere in your class:
dynamic private func audioRouteChangeListener(notification:NSNotification) {
let audioRouteChangeReason = notification.userInfo![AVAudioSessionRouteChangeReasonKey] as! UInt
switch audioRouteChangeReason {
case AVAudioSessionRouteChangeReason.NewDeviceAvailable.rawValue:
print("headphone plugged in")
case AVAudioSessionRouteChangeReason.OldDeviceUnavailable.rawValue:
print("headphone pulled out")
default:
break
}
}
Take care!