Tag: Xamarin.iOS

Xamarin iOS Detect if Ringer Is On Silent or Muted

Xamarin iOS Detect if Ringer Is On Silent or Muted

There’s currently no native support for detecting whether or not the device ringer is set to silent. This poses an issue when you want to leverage the state of the ringer to trigger some action within the app, such as notifying the user.

Fortunately there’s a fairly straightforward way to detect this on your own using a system sound.

Detecting Ringer State by Playing a System Sound

When the ringer is on silent it prevents any system sounds you attempt to play from actually playing. Conversely when the ringer is not on silent the sound will play as expected. We can leverage this behavior to confidently determine whether or not the device is on silent based on how quickly our system sound finishes playing.

Add Muted Sound File to Resources

The first thing we need to do is obtain a copy of a sound file that doesn’t actually make any noise. Here’s one for convenience. After downloading the file add it to the Resources folder in your iOS project.

I added mine under Resources > Audio:

Audio Resources Folder

Write Code to Determine Playback Time of System Sound

Now that we have the file in place we need to create a System Sound and determine how fast the sound played.

The SystemSound object has an observer method called AddSystemSoundCompletion, which as the name suggests lets us know when the sound finishes playing. The idea here is simple: find the time delta between when the sound started playing and when it finished. If the difference in time is negligible than we can safely assume the ringer is on silent. However, if the time delta is closer to how long the sound file would typically take to play, it’s likely the ringer is not on silent.

The following method can be added to your view controller or other class that needs to detect the state of the ringer and take action:

public void IsMuted(Action mutedCheckComplete) {
    // create an instance of the SystemSound object, pointing to your "mute" sound resource
    var soundFilePath = NSBundle.MainBundle.PathForResource("Audio/mute", "caf");
    var mutedSound = new SystemSound(new NSUrl(soundFilePath, false));

    // capture the start time of the sound
    DateTime startTime = DateTime.Now;

    mutedSound.AddSystemSoundCompletion(() => {
        // find the delta between start and end times to determine if the sound played or was cut short.
        var endTime = DateTime.Now;
        var timeDelta = (endTime - startTime).Milliseconds;
        var muted = timeDelta < 100;

        // perform the callback to the invoker of this method, letting them know we have an answer.
        mutedCheckComplete(muted);
    });

    mutedSound.PlaySystemSound();
}

In my tests the time delta was very close to zero when the ringer was on silent, and closer to ~300ms when not on silent. In the above snippet I consider anything less than 100ms to be considered muted.

Now that the method is created we can invoke it whenever we need to check the status of the ringer. In my case, I use this in the ViewWillAppear method of my view controller. To avoid any unexpected lag between loading the UI and performing this check, I added the method call to the dispatch queue as follows:

DispatchQueue.MainQueue.DispatchAfter(new DispatchTime(DispatchTime.Now, TimeSpan.FromMilliseconds(100)), () => {
    IsMuted((muted) => {
        Debug.WriteLine($"Is Muted: {muted}");
    });
});

There we have it, a fairly trivial approach at determining the state of the ringer.

Xamarin iOS UITableView GetCell Method Called for Invisible Cells

Xamarin iOS UITableView GetCell Method Called for Invisible Cells

In my Xamarin iOS application I’m leveraging the UITableView control to page through a collection of items in a performant manner. The UITableView uses the GetCell method of the UITableViewDataSource delegate to create or reuse instances of cells as they become visible on screen.

Great, that sounds efficient, and it is! However, in my application the GetCell method was being called once for every cell when the UITableView was initialized, including the invisible ones.

What gives?

This post spawned from a stackoverflow question I posted, then immediately found the answer to.

Replicating the Behavior By Example

The UITableView in question was added to a ViewController in the StoryBoard. Within the ViewController’s ViewDidLoad method, I have the following setup:

public override void ViewDidLoad() {
    base.ViewDidLoad();

    var model = new UIColor[] {
        UIColor.Green,
        UIColor.Red,
        UIColor.Magenta,
        UIColor.Cyan,
        UIColor.Blue,
        UIColor.Purple
    };

    SampleTableView.RowHeight = SampleTableView.Frame.Height;
    SampleTableView.Setup(model);
}

In short, a model consisting of a collection of colors is created and passed through to a Setup method on my custom UITableView, SampleTableView.
In addition to the setup, I already know the height of each row ahead of time, so the RowHeight property of the UITableView is set. As you can see, each row will take the full height of the UITableView. Swiping up or down will then reveal the next page.

Here’s what the SampleTableView and related dependencies look like:

public class SampleCell : UITableViewCell {
    public void Setup(UIColor color) {
        BackgroundColor = color;
    }
}

public class SampleTableViewDataSourceDelegate : UITableViewDataSource {
    public SampleTableViewDataSourceDelegate(UIColor[] model) {
        this.model = model;
    }

    private readonly UIColor[] model;

    public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) {
        Debug.WriteLine($"GetCell called. Row {indexPath.Row}");
        var reuseIdentifier = "SampleCell";

        var cell = tableView.DequeueReusableCell(reuseIdentifier) as SampleCell;

        cell = (cell ?? (cell = new SampleCell()));

        cell.Setup(model[indexPath.Row]);

        return cell;
    }

    public override nint NumberOfSections(UITableView tableView) {
        return 1;
    }

    public override nint RowsInSection(UITableView tableView, nint section) {
        return model.Length;
    }
}

public partial class SampleTableView : UITableView {
    public SampleTableView(IntPtr handle) : base(handle) {
        PagingEnabled = true;
    }

    private UIColor[] model;

    public void Setup(UIColor[] model) {
        this.model = model;

        DataSource = new SampleTableViewDataSourceDelegate(model);
    }
}

SampleCell
A custom cell that simply sets the background color to the one provided by the model.

SampleTableViewDataSourceDelegate
A derived class of UITableViewDataSource, which handles implementing the GetCell method. This class is responsible creating and reusing cells as they become visible.

SampleTableView
A derived class of UITableView, which handles initial setup of the UITableView via the Setup method, invoked by the containing ViewController.

We now have a fairly slimmed down example of a UITableView implementation. In the GetCell delegate method there’s a line of code that outputs a message to the log when it’s called. After running the application with Visual Studio in a simulator, the Visual Studio Output console shows the following:

[0:] GetCell called. Row 0
[0:] GetCell called. Row 1
[0:] GetCell called. Row 2
[0:] GetCell called. Row 3
[0:] GetCell called. Row 4
[0:] GetCell called. Row 5

Therein lies the issue; the GetCell method is called once for each cell in our model. Why? Didn’t I specify the RowHeight to be the height of the UITableView? If that’s true, and we know that GetCell should only be called when cells become visible, then why is it being called Model.Length number of times?

UITableView EstimatedRowHeight

Our RowHeight property is set to take up the entire UITableView. When the UITableView data source is initialized, however, the UITableView attempts to estimate the required row height for us on-the-fly via the EstimatedRowHeight property. The EstimatedRowHeight property was not set, effectively relying on the table view to calculate it for us. The solution to this particular problem then is to set the EstimateRowHeight property in addition to the RowHeight property.

Back in the ViewDidLoad method of the ViewController, adding the following line of code above the RowHeight setter did the trick:

 SampleTableView.EstimatedRowHeight = SampleTableView.Frame.Height; 

The Output log now shows what we’d expect, a single call to GetCell on initialize:

[0:] GetCell called. Row 0