'
I have a problem with GKTurnBasedMatchmakerViewController. When I display GKTurnBasedMatchmakerViewController and then I press "Play Now" button to find automatch. While Its processing I press the cancel Button after this application crashes.. It never comes in didFindMatch function.. It crashes before that.
Can we disable the cancel button while once "Play Now" Pressed?
Following is my code for GKTurnBasedMatchmakerViewControllerDelegate
`
#pragma GKTurnBasedMatchmakerViewControllerDelegate functions
// The user has cancelled
- (void)turnBasedMatchmakerViewControllerWasCancelled:(GKTurnBasedMatchmakerViewController *)viewController {
[presentingViewController dismissModalViewControllerAnimated:YES];
if([self.delegate respondsToSelector:#selector(gameCenterViewControllrRemoved)]) {
[self.delegate gameCenterViewControllrRemoved];
}
self.delegate = nil;
presentingViewController = nil;
}
// Matchmaking has failed with an error
- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFailWithError:(NSError *)error {
[presentingViewController dismissModalViewControllerAnimated:YES];
if([self.delegate respondsToSelector:#selector(gameCenterViewControllrRemoved)]) {
[self.delegate gameCenterViewControllrRemoved];
}
self.delegate = nil;
presentingViewController = nil;
}
// A turned-based match has been found, the game should start
- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFindMatch:(GKTurnBasedMatch *)match
{
[presentingViewController dismissModalViewControllerAnimated:YES];
[self setCurrentMatch:match];
GKTurnBasedParticipant *firstParticipant = [match.participants objectAtIndex:0];
if(self.delegate) {
if (firstParticipant.lastTurnDate == NULL) {
// It's a new game!
if ([self.delegate respondsToSelector:#selector(startTurnBasedGame)]) {
[self.delegate startTurnBasedGame];
}
//[self.delegate startTurnBasedGame];
} else {
if ([match.currentParticipant.playerID isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
// It's your turn!
if ([self.delegate respondsToSelector:#selector(takeTurn)]) {
[self.delegate takeTurn];
}
} else {
// It's not your turn, just display the game state.
if ([self.delegate respondsToSelector:#selector(displayLayout)]) {
[self.delegate displayLayout];
}
}
}
}
}
`
thanks
chishti
Related
Hope that the issue goes to the correct stream.
I play HLS, i.e. I use AVPlayer for playing a mixed content( audio & pure video) from a server.
I keep playing audio after moving the app into background: I have enabled such feature in plist, plus I do store Prev Player, disable visual tracks, and set to nil AVPlayerLayer.
Here are the methods:
- (void)changePlayerState:(BOOL)restored
{
if (restored && !self.storedPlayer)
return;
if (!restored){
self.storedPlayer = self.playerView.player;
[self.playerView setPlayer:nil];
} else if (self.storedPlayer) {
[self.playerView setPlayer:self.storedPlayer];
self.storedPlayer = nil;
}
AVPlayerItem *playerItem = self.playerView.playerItem;
NSArray *tracks = [playerItem tracks];
for (AVPlayerItemTrack *playerItemTrack in tracks)
{
//
if ([playerItemTrack.assetTrack hasMediaCharacteristic:AVMediaCharacteristicVisual])
playerItemTrack.enabled = restored; //
}
}
And following one:
- (void)setPlayer:(AVPlayer *)player
{
if (_player != player) {
[self removePlayerObservers];
_player = player;
[self configurePlayerLayer];
if (_player)
[self addPlayerObservers];
}
}
- (void)removePlayerObservers
{
if ([_player observationInfo] != nil) {
[_player removeObserver:self
forKeyPath:#"rate"
context:NPUIPlayerViewPlayerRateObservationContext];
[_player removeObserver:self
forKeyPath:#"isExternalPlaybackActive"
context:NPUIPlayerViewAirPlayVideoActiveObservationContext];
}
[self removePeriodicTimeObserver];
}
- (void)addPlayerObservers {
[_player addObserver:self
forKeyPath:#"rate"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:NPUIPlayerViewPlayerRateObservationContext];
[_player addObserver:self
forKeyPath:#"isExternalPlaybackActive"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:NPUIPlayerViewAirPlayVideoActiveObservationContext];
[self addPeriodicTimeObserver];
}
- (void)removePeriodicTimeObserver
{
if (_registeredTimeObserver) {
[_player removeTimeObserver:_registeredTimeObserver];
_registeredTimeObserver = nil;
}
}
- (void)addPeriodicTimeObserver
{
__weak NPUIPlayerView *weakSelf = self;
_registeredTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.5f, 10) queue:dispatch_get_main_queue()
So after moving the app to the background , pressing HOme button, and activating Control Center(sliding up from the button, where is a calculator, camera, light buttons) , I can set the title of item being played, but apple's player controls doesn't work : pressing pause, play doesn't work also slider doesn't work.
I do receive events when I press the buttons, but at the same time, inspite of the fact, that I am calling [player pause], or change rate it doesn't change the button. But when there is an issue with accessing audio via internet, or sound buffer is empty, the button is changed, it has paused UI.
Below goes the code for setting now playing media center and adding commands
- (void)adjustNowPlayingScreen
{
MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
NSMutableDictionary *mDic =[NSMutableDictionary dictionary];
NSString *newTitle = self.playerTitle.text ?: self.itemToPlay.shortName ?: self.itemToPlay.name ?: #"";
if (newTitle.length)
mDic[MPMediaItemPropertyTitle] = newTitle;
AVPlayerItem * item = [self.playerView.player currentItem];
CMTime itemDuration = [self.playerView.player.currentItem duration];
if (CMTIME_IS_VALID(itemDuration) ) {
NSTimeInterval duration = CMTimeGetSeconds(itemDuration);
if (duration)
mDic[MPMediaItemPropertyPlaybackDuration] = #(duration);
}
else
{
NSTimeInterval duration = CMTimeGetSeconds([item.asset duration]);
if (!isnan(duration) && duration > 0)
mDic[MPMediaItemPropertyPlaybackDuration] = #(duration);
else {
duration = CMTimeGetSeconds([[[[self playerView] playerItem] asset] duration]);
if (!isnan(duration) && duration > 0)
mDic[MPMediaItemPropertyPlaybackDuration] = #(duration);
}
}
NSString *urlStr = self.itemToPlay.autoQualityURL ?: self.itemToPlay.lowAutoQualityURL;
if (urlStr.length)
mDic[MPMediaItemPropertyAssetURL] = urlStr;
CMTime curTime = self.playerView.playerItem.currentTime;
if (CMTIME_IS_VALID(curTime)) {
NSTimeInterval duration = CMTimeGetSeconds(curTime);
mDic[MPNowPlayingInfoPropertyElapsedPlaybackTime]= #(duration);
}
if (mDic.count) {
#warning #"HACK: Necessary to change number of played items & index"
mDic[ MPMediaItemPropertyAlbumTrackNumber] = #(1);
mDic[ MPMediaItemPropertyAlbumTrackCount] = #(1);
mDic[ MPNowPlayingInfoPropertyPlaybackRate] = #(self.playerView.isPlaying ? 1.0 : 0.0);
UIImage *img = [UIImage imageNamed:#"team_4f6ae0b99d4b856118000124"];
mDic[MPMediaItemPropertyArtwork] = [[MPMediaItemArtwork alloc]initWithImage:img];
infoCenter.nowPlayingInfo = mDic;
}
}
- (void)addRemoteCommands
{
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
MPRemoteCommand *command = [commandCenter pauseCommand];
command.enabled = true;
[command addTarget:self action:#selector(pauseCommand:)];
command = [commandCenter playCommand];
command.enabled = true;
[command addTarget:self action:#selector(playCommand:)];
command = [commandCenter togglePlayPauseCommand];
command.enabled = true;
[command addTarget:self action:#selector(toggleCommand:)];
command = [commandCenter seekForwardCommand];
command.enabled = true;
[command addTarget:self action:#selector(seekForwardCommand:)];
command = [commandCenter seekBackwardCommand];
command.enabled = true;
[command addTarget:self action:#selector(seekBackwardCommand:)];
}
- (void)updateElapsedTime
{
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
MPRemoteCommand *command = [commandCenter togglePlayPauseCommand];
if (!command.enabled)
return;
[self adjustNowPlayingScreen];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 2);
dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void){
[self updateElapsedTime];
});
}
- (void)removeRemoteCommands
{
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
MPRemoteCommand *command = [commandCenter pauseCommand];
command.enabled = false;
[command removeTarget:self action:#selector(pauseCommand:)];
command = [commandCenter playCommand];
command.enabled = false;
[command removeTarget:self action:#selector(playCommand:)];
command = [commandCenter togglePlayPauseCommand];
command.enabled = false;
[command removeTarget:self action:#selector(toggleCommand:)];
command = [commandCenter seekForwardCommand];
command.enabled = false;
[command removeTarget:self action:#selector(seekForwardCommand:)];
command = [commandCenter seekBackwardCommand];
command.enabled = false;
[command removeTarget:self action:#selector(seekBackwardCommand:)];
}
- (void)seekBackwardCommand:(MPRemoteCommandEvent *)event
{
NSLog(#"%#",NSStringFromClass([event class]));
}
- (void)seekForwardCommand:(MPRemoteCommandEvent *)event
{
NSLog(#"%#",NSStringFromClass([event class]));
}
- (void)pauseCommand:(MPRemoteCommandEvent *)event
{
[self pause]; //_player pause];
AVPlayerItem *playerItem = self.playerView.playerItem;
NSArray *tracks = [playerItem tracks];
for (AVPlayerItemTrack *playerItemTrack in tracks)
{
/
if ([playerItemTrack.assetTrack hasMediaCharacteristic:AVMediaCharacteristicAudible])
playerItemTrack.enabled = false; /
}
MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
NSMutableDictionary *mDic =[NSMutableDictionary dictionaryWithDictionary:infoCenter.nowPlayingInfo];
mDic[ MPNowPlayingInfoPropertyPlaybackRate] = #(0.0);
infoCenter.nowPlayingInfo = mDic;
}
So , my pause command is called, I call AVPlayer's pause method. But it doesn't stop audio stream.
And Slider doesn't work, therefore my seekBackwardCommand/seekForwardCommand are not called.
But as I said when the stream is over (audio also) the player on Control Center changes button from playing into pause, i.e. somehow it listen to Audio Session changes.
I adjust the sound category by setting AVAudioSessionCategoryPlayback, and setting mode : AVAudioSessionModeMoviePlayback
Please help how properly to handle pause/ play buttons on Control Screen, how to enable slider?
I am running my code on iOS9, iPhone 6.
In my project, I use AVAudioSession to detect any headphone is plugged or unplugged. But in this case, I can't detect when bluetooth device is plugged. Here is my code for headphone state.
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification
{
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
//NSLog(#"AVAudioSessionRouteChangeReasonNewDeviceAvailable");
NSLog(#"Headphone/Line plugged in");
[_soundButtonOutlet setImage:[UIImage imageNamed:#"sound-on.png"] forState:UIControlStateNormal];
_headSetState=YES;
break;
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
NSLog(#"AVAudioSessionRouteChangeReasonOldDeviceUnavailable");
NSLog(#"Headphone/Line was pulled. Stopping player....");
[_soundButtonOutlet setImage:[UIImage imageNamed:#"sound-off.png"] forState:UIControlStateNormal];
if(_isPlaying==YES)
{
[self.player pause];
[_audioButtonOutlet setImage:[UIImage imageNamed:#"play.png"] forState:UIControlStateNormal];
_isPlaying=NO;
}
_headSetState=NO;
break;
case AVAudioSessionRouteChangeReasonCategoryChange:
// called at start - also when other audio wants to play
NSLog(#"AVAudioSessionRouteChangeReasonCategoryChange");
break;
}
- (BOOL)isHeadsetPluggedIn
{
AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
for (AVAudioSessionPortDescription* desc in [route outputs]) {
if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones])
{
[_soundButtonOutlet setImage:[UIImage imageNamed:#"sound-on.png"] forState:UIControlStateNormal];
_headSetState=YES;
return YES;
}
else
{
[_soundButtonOutlet setImage:[UIImage imageNamed:#"sound-off.png"] forState:UIControlStateNormal];
_headSetState=NO;
return NO;
}
}
return NO;
}
}
- viewWillAppear {
[AVAudioSession sharedInstance];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
[self isHeadsetPluggedIn];
}
So how can I detect if a bluetooth headset plugged or not iOS 8?
You can detect currently active bluetooth output devices (instead of input devices)
Swift Code:
import AVFoundation
func bluetoothAudioConnected() -> Bool{
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
for output in outputs{
if output.portType == AVAudioSessionPortBluetoothA2DP || output.portType == AVAudioSessionPortBluetoothHFP || output.portType == AVAudioSessionPortBluetoothLE{
return true
}
}
return false
}
Bluetooth devices are based on the following question: What's the difference among AVAudioSessionPortBluetoothHFP, A2DP and LE?
I hope it helps someone
Edit for Swift 5.1 (Thanks iago849 for the fix)
var bluetoothDeviceConnected: Bool {
!AVAudioSession.sharedInstance().currentRoute.outputs.compactMap {
($0.portType == .bluetoothA2DP ||
$0.portType == .bluetoothHFP ||
$0.portType == .bluetoothLE) ? true : nil
}.isEmpty
}
I was able to detect whether a bluetooth headset (HFP) device was currently connected using the following:
NSArray *arrayInputs = [[AVAudioSession sharedInstance] availableInputs];
for (AVAudioSessionPortDescription *port in arrayInputs)
{
if ([port.portType isEqualToString:AVAudioSessionPortBluetoothHFP])
{
bHas = YES;
break;
}
}
However, your AVAudioSession category must be set as AVAudioSessionCategoryPlayAndRecord in order for this to work. If it isn't, the port will not show up in the list even if the HFP device is connected.
You can detect it with routeChangeNotification:
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
}
if reason == .newDeviceAvailable {
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.bluetoothA2DP {
print("Bluetooth Headphone Connected")
break
}
}
}
How to edit UITextFiled in Iphone,once i have type something but we can't edit it from textfield.
please help me.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if([tfieldDOB.text length] == 4)
{
tfieldDOB.text=[NSString stringWithFormat:#"%#/",tfieldDOB.text];
}
else if([tfieldDOB.text length]==7)
{
tfieldDOB.text=[NSString stringWithFormat:#"%#/",tfieldDOB.text];
}
return YES;
}
You must use UITextFieldDelegate.
#interface KInitUserViewController : UIViewController <UITextFieldDelegate>
implement delegates...
// Delegate UITextField
- (BOOL)disablesAutomaticKeyboardDismissal {
return NO;
}
-(BOOL) textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
//here you can submit your changes....
return YES;
}
Try below code it will work for you.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (textField == tfieldDOB) {
if ([string isEqualToString:#""]) {// This codition for while taping back space on the keyboard
return YES;
}
NSString *currentText = [textField.text stringByReplacingCharactersInRange:range withString:string];
if (currentText.length == 4 || currentText.length == 7) {
textField.text = [NSString stringWithFormat:#"%#/",currentText];
return NO;
}
}
return YES;
}
I am using AVAssetWriter/AVAssetReader to transcode a PCM audio file to AAC. I have boiled it down to a simple project that works in iOS6 and fails in iOS7.
Every thing is going well until I get to [self.assetWriter finishWritingWithCompletionHandler:] Then the assetWriter goes into the failed state with the error set to -11800 AVFoundation unknown error with an internal error set to -12733 which apparently corresponds to SampleBufferNotReady.
dispatch_queue_t queue = dispatch_queue_create("audio.encode", DISPATCH_QUEUE_SERIAL);
success = [self.assetWriter startWriting];
if (!success)
{
[self showStatus:#"Export: writer failed to startWriting"];
return;
}
[self.assetWriter startSessionAtSourceTime:kCMTimeZero];
[assetWriterInput requestMediaDataWhenReadyOnQueue:queue
usingBlock:
^{
while([assetWriterInput isReadyForMoreMediaData])
{
NSAssert (self.assetWriter.status == AVAssetWriterStatusWriting, nil);
CMSampleBufferRef sampleBuffer = [assetReaderOutput copyNextSampleBuffer];
if (sampleBuffer)
{
NSAssert (CMSampleBufferIsValid(sampleBuffer), nil);
NSAssert (CMSampleBufferDataIsReady(sampleBuffer), nil);
BOOL success = [assetWriterInput appendSampleBuffer:sampleBuffer];
if (!success)
{
[self showError:self.assetWriter.error];
self.assetWriter = nil;
CFRelease(sampleBuffer);
return;
}
CFRelease(sampleBuffer);
}
else
{
if ([assetReader status] == AVAssetReaderStatusCompleted)
{
[assetWriterInput markAsFinished];
[self.assetWriter finishWritingWithCompletionHandler:^{
BOOL success = self.assetWriter.status == AVAssetWriterStatusCompleted;
if (success)
{
[self showStatus: #"Did it!"];
self.assetWriter = nil;
}
else
{
[self showError:self.assetWriter.error];
self.assetWriter = nil;
}
}];
}
else
{
[self showError:assetReader.error];
self.assetWriter = nil;
}
}
}
}
];
Note: I have posted a bug with Apple, posted to the dev forum and used a TSI. Haven't got any answers yet. My hope is one of you geniuses will point me to a workaround.
i have same problem with you, but finally i solve that problem, i use this method :
CMTime cmTime = CMTimeMake(longDuration, 1);
[assetWriter endSessionAtSourceTime:cmTime];
[assetWriter finishWritingWithCompletionHandler^(){
NSLog (#"finished writing");
];
usually we don't need call this if we call finishWritingWithCompletionHandler;
i hope this solve your problem.
I have implemented a UISearchBar into a table view and almost everything is working except one small thing: When I enter text and then press the search button on the keyboard, the keyboard goes away, the search results are the only items shown in the table, the text stays in the UISearchBar, but the cancel button gets disabled.
I have been trying to get my list as close to the functionality of the Apple contacts app and when you press search in that app, it doesn't disable the cancel button.
When I looked in the UISearchBar header file, I noticed a flag for autoDisableCancelButton under the _searchBarFlags struct but it is private.
Is there something that I am missing when I setup the UISearchBar?
I found a solution. You can use this for-loop to loop over the subviews of the search bar and enable it when the search button is pressed on the keyboard.
for (UIView *possibleButton in searchBar.subviews)
{
if ([possibleButton isKindOfClass:[UIButton class]])
{
UIButton *cancelButton = (UIButton*)possibleButton;
cancelButton.enabled = YES;
break;
}
}
I had to tweak this a bit to get it work for me in iOS7
- (void)enableCancelButton:(UISearchBar *)searchBar
{
for (UIView *view in searchBar.subviews)
{
for (id subview in view.subviews)
{
if ( [subview isKindOfClass:[UIButton class]] )
{
[subview setEnabled:YES];
NSLog(#"enableCancelButton");
return;
}
}
}
}
There is two way to achieve this easily
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar{
// The small and dirty
[(UIButton*)[searchBar valueForKey:#"_cancelButton"] setEnabled:YES];
// The long and safe
UIButton *cancelButton = [searchBar valueForKey:#"_cancelButton"];
if ([cancelButton respondsToSelector:#selector(setEnabled:)]) {
cancelButton.enabled = YES;
}
}
You should go with the second one, it will not crash your application if Apple will change it in the background.
BTW i tested it from iOS 4.0 to 8.2 and no changes, also i use it in my Store approved application without any issues.
This is what made it to work on iOS 6 for me:
searchBar.showsCancelButton = YES;
searchBar.showsScopeBar = YES;
[searchBar sizeToFit];
[searchBar setShowsCancelButton:YES animated:YES];
Here's my solution, which works for all situations in all versions of iOS.
IE, other solutions don't handle when the keyboard gets dismissed because the user dragged a scroll view.
- (void)enableCancelButton:(UIView *)view {
if ([view isKindOfClass:[UIButton class]]) {
[(UIButton *)view setEnabled:YES];
} else {
for (UIView *subview in view.subviews) {
[self enableCancelButton:subview];
}
}
}
// This will handle whenever the text field is resigned non-programatically
// (IE, because it's set to resign when the scroll view is dragged in your storyboard.)
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar {
[self performSelector:#selector(enableCancelButton:) withObject:searchBar afterDelay:0.001];
}
// Also follow up every [searchBar resignFirstResponder];
// with [self enableCancelButton:searchBar];
As per my answer here, place this in your searchBar delegate:
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
dispatch_async(dispatch_get_main_queue(), ^{
__block __weak void (^weakEnsureCancelButtonRemainsEnabled)(UIView *);
void (^ensureCancelButtonRemainsEnabled)(UIView *);
weakEnsureCancelButtonRemainsEnabled = ensureCancelButtonRemainsEnabled = ^(UIView *view) {
for (UIView *subview in view.subviews) {
if ([subview isKindOfClass:[UIControl class]]) {
[(UIControl *)subview setEnabled:YES];
}
weakEnsureCancelButtonRemainsEnabled(subview);
}
};
ensureCancelButtonRemainsEnabled(searchBar);
});
}
None of the answers worked for me at all. I'm targeting iOS 7. But I found an answer.
What I'm trying is something like the Twitter iOS app. If you click on the magnifying glass in the Timelines tab, the UISearchBar appears with the Cancel button activated, the keyboard showing, and the recent searches screen. Scroll the recent searches screen and it hides the keyboard but it keeps the Cancel button activated.
This is my working code:
UIView *searchBarSubview = self.searchBar.subviews[0];
NSArray *subviewCache = [searchBarSubview valueForKeyPath:#"subviewCache"];
if ([subviewCache[2] respondsToSelector:#selector(setEnabled:)]) {
[subviewCache[2] setValue:#YES forKeyPath:#"enabled"];
}
I arrived at this solution by setting a breakpoint at my table view's scrollViewWillBeginDragging:. I looked into my UISearchBar and bared its subviews. It always has just one, which is of type UIView (my variable searchBarSubview).
Then, that UIView holds an NSArray called subviewCache and I noticed that the last element, which is the third, is of type UINavigationButton, not in the public API. So I set out to use key-value coding instead. I checked if the UINavigationButton responds to setEnabled:, and luckily, it does. So I set the property to #YES. Turns out that that UINavigationButton is the Cancel button.
This is bound to break if Apple decides to change the implementation of a UISearchBar's innards, but what the hell. It works for now.
Here's a Swift 3 solution that makes use of extensions to get the cancel button easily:
extension UISearchBar {
var cancelButton: UIButton? {
for subView1 in subviews {
for subView2 in subView1.subviews {
if let cancelButton = subView2 as? UIButton {
return cancelButton
}
}
}
return nil
}
}
Now for the usage:
class MyTableViewController : UITableViewController, UISearchBarDelegate {
var searchBar = UISearchBar()
func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
tableView.tableHeaderView = searchBar
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
DispatchQueue.main.async {
if let cancelButton = searchBar.cancelButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
}
}
}
}
For Monotouch or Xamarin iOS I have the following C# solution working for iOS 7 and iOS 8:
foreach(UIView view in searchBar.Subviews)
{
foreach(var subview in view.Subviews)
{
//Console.WriteLine(subview.GetType());
if(subview.GetType() == typeof(UIButton))
{
if(subview.RespondsToSelector(new Selector("setEnabled:")))
{
UIButton cancelButton = (UIButton)subview;
cancelButton.Enabled = true;
Console.WriteLine("enabledCancelButton");
return;
}
}
}
}
This answer is based on David Douglas solution.
A more complete answer:
since iOS 7, there is an additional level of subviews under the searchBar
a good place to enable the cancel button is in searchBarTextDidEndEditing
.
extension MyController: UISearchBarDelegate {
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
DispatchQueue.main.async {
// you need that since the disabling will
// happen after searchBarTextDidEndEditing is called
searchBar.subviews.forEach({ view in
view.subviews.forEach({ subview in
// ios 7+
if let cancelButton = subview as? UIButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
return
}
})
// ios 7-
if let cancelButton = subview as? UIButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
return
}
})
}
}
}
Time passes, but the problem is still there...
Elegant Swift 5/iOS 13 solution:
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
for case let cancelButton as UIButton in searchBar.subviews {
cancelButton.isEnabled = true
}
}