Play audio while silenced or through Receiver

AVAudioSession and AVAudioPlayer

To take full control over the sound routing and when
Play audio through Receiver (earpiece speaker)

Link to gist with extension I’ve created to deal with category changes.

UITabBarController inside Master View of UISplitViewController

An iPhone only chat application.

If you need to create a Messenger/Telegram/WhatsApp like interface for iPhone you can start by simply taking Xcode provided Tabbed Application template and personalize each view controller as desired to get your interface flow working. This includes various transition by segue or pushViewController, device rotation, autosizing etc…

A Universal chat application.

For a Universal iPhone and iPad chat application it becomes more complicated, this isn’t that simple…
iOS SDK isn’t really good in providing acceptable default behavior when you combine some simple interface elements to obtain a complex one. For a Universal app we must modify the Storyboard by adding UISplitViewController, make it the Storyboard Entry Point and move the UITabBarController in such way to replace Master View Controller. Or start with Master-Detail Application example and go the other way around. Choosing the later one you must end with something similar to this storyboard:
Storyboard with UITabBarController inside MasterView of UISplitViewController

I aim to misbehave.

On an iPad, this setup works pretty well, but on iPhone and iPhone Plus the default implementation tends to misbehave terribly. In a compact horizontal size class, like in portrait and landscape orientation on iPhone or in portrait orientation on iPhone Plus, when you tap on a table view item the detail view controller is presented as a modal popover with no controls to close it. This is because UITabBarController, placed as replacement of UINavigationController of Primary view of UISplitViewController, to behave correctly, must override -showViewController:sender: method called by UISplitViewController in horizontally compact size. But it doesn’t and default implementation presents the incoming detail view controller modally.

We can solve this in three ways:

  • by subclassing UITabBarController and override -showViewController:sender:
  • by subclassing UISplitViewControllerDelegate and override -showDetailViewController:sender:
  • by implementing the UISplitViewControllerDelegate optional protocol method -splitViewController:showDetailViewController:sender: of UISplitViewController delegate

All solutions are similar, when show[Detail]ViewController is called, you need to take the incoming UIViewController, cast it to UINavigationController, get the topViewController and push it on selectedViewController of your UITabBarController. Code speaks better:

Now we are good in portrait orientation, but try to rotate iPhone Plus to landscape and you will see that the detail view controller is on the left side of the split view controller and not on the right where it must be… This is again because UITabBarController is asked to handle the transition to not collapsed regular horizontal size class via -separateSecondaryViewControllerFromPrimaryViewController: but it doesn’t implement it.

Compact to Regular

As above we can solve this in three ways. General steps are to check if the topmost view controller is a DetailViewController, if so pop it from the navigation stack, embed it in a UINavigationController and return the result to UISplitViewController.

And back to Compact

Now if you rotate iPhone Plus back from landscape to portrait you will lose the DetailViewController from your navigation stack. To preserve it you must manually push it back to one of the UITabBarController views UINavigationController‘s.

Debug tip

To see yourself how controller hierarchy change with various screen sizes and with orientation change, place a breakpoint in each of delegate calls and print the whole view controller hierarchy in LLDB console with po [[[UIWindow keyWindow] rootViewController] _printHierarchy].

Play audio for UI sound effects

Water ripple
Sound feedback

Normally you need to play a simple UI sound effect as a feedback reaction to some user input or to notify the user about something happening. It’s a perfect case to use the AudioServices from AudioToolbox framework. It’s simple, straightforward, really easy to use and with 0 hassle doesn’t play any sound if the device is in silenced mode.

System Sound Services

There are three simple steps to play audio using AudioServices:

  1. Designate an audio file for playback by obtaining the SystemSoundID from System Sound server
  2. Play the sound by passing to System Sound server the designated SystemSoundID
  3. Dispose of any resources allocated to the playback for the provided SystemSoundID

As per documentation, the audio file must be packaged in a .caf, .aif, or .wav file, but I’ve found that since iOS 7 and up you can also use .mp3 packaging.

If you need to play the same sound frequently, save the SystemSoundID as you wish, pass nil to the completion block of AudioServicesPlaySystemSoundWithCompletion and dispose the resource in an appropriate place like view unload.

You can also take note of OSStatus returned by each function and if it’s different than kAudioServicesNoError look at AudioServices error codes to see whats happening.

Tip: even in silenced mode you can still play a vibration “sound” with AudioServicesPlaySystemSound(kSystemSoundID_Vibrate).

Limitations

But there are some drawbacks of AudioServices that can force you to look for another solution. It is limited to play sounds of a maximum duration of 30 seconds, there is no programmatic volume control, you can’t play more than one sound at a time and iOS has full control of what route the sound will take and if it will play at all.

Next time we explore how to gain power and control with the AVFoundation framework.

Programmaticaly add constraints to layout guides

Top layout guide
Top and Bottom Layout Guides.

They represent the upper and lower limit from where you can safely layout custom views or UIKit toolbar/search bar in your view controller, without worrying that the status bar, navigation bar, or tab bar will cover them under top and bottom extended layout edges. Using storyboard Xcode’s Interface Builder will automatically add them inside a view controller scene for easy pinning with view’s top or bottom edge constraints. But what if you don’t use storyboard?

View controller without storyboard.

There are many cases when you need to account for the top and bottom layout guides in building a view with interface builder outside storyboard. The most common case is when you are still instantiating a view controller with NIB/XIB as the view.

Programmatical approach.

Let’s see how to pin a simple UISearchBar in a view controller under the embedding navigation controller’s navigation bar. After you have completely laid out all the view and pinned all edges, select the constraint that pin the top of the search bar to top of the superview and tick the “Placeholder” checkbox to indicate that it must be removed at build time.

Placeholder constraint

You must do this because later we will programmatically add a constraint that pins the same top of the search bar to top layout guide. If Auto Layout engine while doing it’s magic find two constraints that pin the same edge to different places it will raise an exception saying “Unable to simultaneously satisfy constraints”.

While you are in Interface Builder create an outlet for the search bar, mine is called SrchBar_Header.

Now somewhere in viewDidLoad add this piece of code:

And that’s it, enjoy the automatically adjusted position of the search bar when rotating to landscape compact size class and back to normal!

Hex color to UIColor

Coffe on table and Hex palette

When your UX designer gives you a palette of Hexadecimal colors to assign at random to each user in the contact list and you already know that this color palette may change in the future, you start by breaking the problem into small parts.

Hexadecimal strings

Let’s assume that you load your Hex colors as strings from some file (in plain text with separators or in a JSON) and put them in an array.

Hex color in #FFFFFF format is in fact a 0xFFFFFF hexadecimal integer. So, the general idea behind the following code is to transform hexadecimal represented string value into an unsigned integer. In this way you can also reuse it in the other part of your code to more general purposes. The best way to do it is using NSScanner class and the corresponding scanHexInt: method.

As per documentation “The hexadecimal integer representation may optionally be preceded by 0x or 0X” we don’t need to check for 0x or 0X preceding the strings because scanHexInt: will do it for us and skip them automatically.

Returning result of this function is an NSArray with all you hex integer numbers converted from respective strings.

Random color from array

We must assign a random color to each contact, but to be associated and recognized by the user it must be the same for each application launch. We can surely generate a random number and save it somewhere in DB alongside contact data, but why complicate it so much if we can directly use contact data to do this?

For exercise wrap it in a function that receives a string to hash with the array of hex numbers and as a result return a hex integer.

Get the UIColor

The final step is to convert our hexadecimal color integer that we choose randomly into a valid UIColor. To do this you can write a dedicated function or #define a simple macro

And use it like this

or the full UIColorFromRGBA and specify also the desired Alpha value in [0, 255] range.

How to get current UIBarMetrics value

Navigation signage
actually

you can’t… or at least I can’t find a way to get UIBarMetrics either from self.navigationBar or from self.navigationItem. There is simply no public getter methods in all UINavigationBar related API’s!

What can we do?

By relying on UITraitCollection and getting horizontal and vertical size classes we can deduce if we are in a regular or a compact condition. App’s navigation bar enters in compact metrics when both horizontal and vertical size classes are compact.

This way we cover UIBarMetricsDefault and UIBarMetricsCompact, leaving to the further deduction UIBarMetricsDefaultPrompt and UIBarMetricsCompactPrompt. But at least it’s something!

If you also need to adapt dynamically when the iOS interface environment changes (e.g. when the device is rotated), then you need to listen to UIViewController conforming UITraitEnvironment protocol delegate callback traitCollectionDidChange: