Xamarin.iOS'ta CallKit
iOS 10'daki yeni CallKit API,VOIP uygulamalarının i Telefon kullanıcı arabirimiyle tümleştirilmesine ve son kullanıcıya tanıdık bir arabirim ve deneyim sağlamasına yönelik bir yol sağlar. Bu API ile kullanıcılar iOS cihazının Kilit Ekranından VOIP çağrılarını görüntüleyebilir ve bunlarla etkileşimde bulunabilir ve Telefon uygulamasının Sık Kullanılanlar ve Son Kullanılanlar görünümlerini kullanarak kişileri yönetebilir.
CallKit hakkında
Apple'a göre CallKit, 3. taraf IP Üzerinden Ses (VOIP) uygulamalarını iOS 10'da birinci taraf deneyimine yükseltecek yeni bir çerçevedir. CallKit API'si, VOIP uygulamalarının i Telefon kullanıcı arabirimiyle tümleştirilmesine ve son kullanıcıya tanıdık bir arabirim ve deneyim sağlamasına olanak tanır. Yerleşik Telefon uygulamasında olduğu gibi, bir kullanıcı iOS cihazının Kilit Ekranından VOIP çağrılarını görüntüleyebilir ve bunlarla etkileşimde bulunabilir ve Telefon uygulamasının Sık Kullanılanlar ve Son Kullanılanlar görünümlerini kullanarak kişileri yönetebilir.
Buna ek olarak, CallKit API'si bir telefon numarasını bir adla ilişkilendirebilen (Arayan Kimliği) veya sisteme bir numaranın ne zaman engellenmesi gerektiğini (Arama Engelleme) söyleyebilen Uygulama Uzantıları oluşturma olanağı sağlar.
Mevcut VOIP uygulama deneyimi
Yeni CallKit API'sini ve yeteneklerini tartışmadan önce MonkeyCall adlı kurgusal bir VOIP uygulaması kullanarak iOS 9'da (ve daha küçük) 3. taraf VOIP uygulamasıyla geçerli kullanıcı deneyimine göz atın. MonkeyCall, kullanıcının mevcut iOS API'lerini kullanarak VOIP çağrıları gönderip almasını sağlayan basit bir uygulamadır.
Şu anda, kullanıcı MonkeyCall'da gelen bir çağrı alıyorsa ve i Telefon kilitlenmişse, Kilit ekranında alınan bildirim diğer herhangi bir bildirim türünden ayırt edilemez (örneğin İletiler veya Posta uygulamalarından gelenler gibi).
Kullanıcının aramayı yanıtlamak istemesi durumunda, aramayı kabul edip konuşmayı başlatabilmek için monkeycall bildirimini kaydırarak uygulamayı açması ve telefonun kilidini açmak için geçiş kodunu (veya kullanıcı Touch ID'sini) girmesi gerekir.
Telefonun kilidi açıksa deneyim de aynı derecede hantaldır. Yine gelen MonkeyCall çağrısı, ekranın üst kısmından içeri kaydırılan standart bir bildirim başlığı olarak görüntülenir. Bildirim geçici olduğundan, kullanıcının Bildirim Merkezi'ni açmaya zorlaması ve yanıtlamak için belirli bir bildirimi bulması ve ardından MonkeyCall uygulamasını el ile arayıp bulup başlatması kolayca kaçırılabilir.
CallKit VOIP uygulama deneyimi
MonkeyCall uygulamasında yeni CallKit API'leri uygulanarak, kullanıcının gelen VOIP çağrısı deneyimi iOS 10'da büyük ölçüde geliştirilebilir. Telefonu yukarıdan kilitlendiğinde VOIP araması alan kullanıcının örneğini inceleyin. CallKit uygulanarak çağrı, tam ekran, yerel kullanıcı arabirimi ve standart yanıt çekme işleviyle yerleşik Telefon uygulamasından çağrı alınırken olduğu gibi i Telefon'nin Kilit ekranında görünür.
Yine, bir MonkeyCall VOIP çağrısı alındığında i Telefon kilidi açılırsa, yerleşik Telefon uygulamasının aynı tam ekran, yerel kullanıcı arabirimi ve standart yanıta doğru çekme ve reddetme işlevselliği sunulur ve MonkeyCall özel bir zil sesi çalma seçeneğine sahiptir.
CallKit, MonkeyCall'a ek işlevsellik sağlayarak VOIP çağrılarının diğer arama türleriyle etkileşim kurmasına, yerleşik Son kullanılanlar ve Sık Kullanılanlar listelerinde görünmesine, yerleşik Rahatsız Etmeyin ve Engelleme özelliklerini kullanmasına, Siri'den MonkeyCall çağrılarını başlatmasına ve kullanıcıların Kişiler uygulamasındaki kişilere MonkeyCall çağrıları atamasına olanak tanır.
Aşağıdaki bölümlerde CallKit mimarisi, gelen ve giden çağrı akışları ve CallKit API'si ayrıntılı olarak ele alınacaktır.
CallKit mimarisi
iOS 10'da Apple, Tüm Sistem Hizmetlerinde CallKit'i benimsemiştir. Örneğin, CarPlay'de yapılan çağrılar CallKit aracılığıyla Sistem Kullanıcı Arabirimi tarafından bilinir. Aşağıda verilen örnekte MonkeyCall, CallKit'i benimsediğinden, Sistem tarafından bu yerleşik Sistem Hizmetleri ile aynı şekilde bilinir ve aynı özelliklerin tümünü alır:
Yukarıdaki diyagramdan MonkeyCall Uygulamasına daha yakından bakın. Uygulama, kendi ağıyla iletişim kurmak için tüm kodunu ve kendi Kullanıcı Arabirimlerini içerir. Sistemle iletişim kurmak için CallKit'e bağlanır:
CallKit'te uygulamanın kullandığı iki ana arabirim vardır:
CXProvider
- Bu, MonkeyCall uygulamasının oluşabilecek bant dışı bildirimleri sisteme bildirmesini sağlar.CXCallController
- MonkeyCall uygulamasının sistemi yerel kullanıcı eylemleri hakkında bilgilendirmesine izin verir.
The CXProvider
Yukarıda belirtildiği gibi, CXProvider
bir uygulamanın oluşabilecek bant dışı bildirimleri sisteme bildirmesine izin verir. Bunlar, yerel kullanıcı eylemleri nedeniyle gerçekleşmeyen ancak gelen çağrılar gibi dış olaylar nedeniyle oluşan bildirimdir.
Bir uygulama aşağıdakiler için şunu CXProvider
kullanmalıdır:
- Gelen bir aramayı Sisteme bildirin.
- Giden aramanın Sisteme bağlandığını bildirin.
- Uzak kullanıcıyı Sistem çağrısını sonlandırarak bildirin.
Uygulama sistemle iletişim kurmak istediğinde sınıfını CXCallUpdate
kullanır ve Sistemin uygulamayla iletişim kurması gerektiğinde sınıfını CXAction
kullanır:
The CXCallController
, CXCallController
bir uygulamanın voip çağrısı başlatan kullanıcı gibi yerel kullanıcı eylemleri hakkında sisteme bilgi vermesine olanak tanır. Bir CXCallController
uygulama uygulayarak sistemdeki diğer çağrı türleriyle etkileşime geçer. Örneğin, devam eden etkin bir telefon araması varsa, CXCallController
VOIP uygulamasının bu aramayı beklemeye alıp VOIP araması başlatmasına veya yanıtlamasına izin verebilir.
Bir uygulama aşağıdakiler için şunu CXCallController
kullanmalıdır:
- Kullanıcının Sisteme giden bir çağrı başlattığını bildirin.
- Kullanıcı Sistem'e gelen bir aramayı yanıtladığında rapor edin.
- Kullanıcının Sistem çağrısını ne zaman sonlandırdığında rapor edin.
Uygulama yerel kullanıcı eylemlerini sisteme iletmek istediğinde sınıfını CXTransaction
kullanır:
CallKit Uygulama
Aşağıdaki bölümlerde, Xamarin.iOS VOIP uygulamasında CallKit'in nasıl uygulanacağı gösterilir. Örneğin, bu belge kurgusal MonkeyCall VOIP uygulamasındaki kodu kullanacaktır. Burada sunulan kod birkaç destekleyici sınıfı temsil eder; CallKit'e özgü bölümler aşağıdaki bölümlerde ayrıntılı olarak ele alınacaktır.
ActiveCall sınıfı
sınıfı, ActiveCall
MonkeyCall uygulaması tarafından şu anda etkin olan bir VOIP çağrısı hakkındaki tüm bilgileri aşağıdaki gibi tutmak için kullanılır:
using System;
using CoreFoundation;
using Foundation;
namespace MonkeyCall
{
public class ActiveCall
{
#region Private Variables
private bool isConnecting;
private bool isConnected;
private bool isOnhold;
#endregion
#region Computed Properties
public NSUuid UUID { get; set; }
public bool isOutgoing { get; set; }
public string Handle { get; set; }
public DateTime StartedConnectingOn { get; set;}
public DateTime ConnectedOn { get; set;}
public DateTime EndedOn { get; set; }
public bool IsConnecting {
get { return isConnecting; }
set {
isConnecting = value;
if (isConnecting) StartedConnectingOn = DateTime.Now;
RaiseStartingConnectionChanged ();
}
}
public bool IsConnected {
get { return isConnected; }
set {
isConnected = value;
if (isConnected) {
ConnectedOn = DateTime.Now;
} else {
EndedOn = DateTime.Now;
}
RaiseConnectedChanged ();
}
}
public bool IsOnHold {
get { return isOnhold; }
set {
isOnhold = value;
}
}
#endregion
#region Constructors
public ActiveCall ()
{
}
public ActiveCall (NSUuid uuid, string handle, bool outgoing)
{
// Initialize
this.UUID = uuid;
this.Handle = handle;
this.isOutgoing = outgoing;
}
#endregion
#region Public Methods
public void StartCall (ActiveCallbackDelegate completionHandler)
{
// Simulate the call starting successfully
completionHandler (true);
// Simulate making a starting and completing a connection
DispatchQueue.MainQueue.DispatchAfter (new DispatchTime(DispatchTime.Now, 3000), () => {
// Note that the call is starting
IsConnecting = true;
// Simulate pause before connecting
DispatchQueue.MainQueue.DispatchAfter (new DispatchTime (DispatchTime.Now, 1500), () => {
// Note that the call has connected
IsConnecting = false;
IsConnected = true;
});
});
}
public void AnswerCall (ActiveCallbackDelegate completionHandler)
{
// Simulate the call being answered
IsConnected = true;
completionHandler (true);
}
public void EndCall (ActiveCallbackDelegate completionHandler)
{
// Simulate the call ending
IsConnected = false;
completionHandler (true);
}
#endregion
#region Events
public delegate void ActiveCallbackDelegate (bool successful);
public delegate void ActiveCallStateChangedDelegate (ActiveCall call);
public event ActiveCallStateChangedDelegate StartingConnectionChanged;
internal void RaiseStartingConnectionChanged ()
{
if (this.StartingConnectionChanged != null) this.StartingConnectionChanged (this);
}
public event ActiveCallStateChangedDelegate ConnectedChanged;
internal void RaiseConnectedChanged ()
{
if (this.ConnectedChanged != null) this.ConnectedChanged (this);
}
#endregion
}
}
ActiveCall
çağrısının durumunu tanımlayan birkaç özellik ve çağrı durumu değiştiğinde tetiklenebilir iki olay içerir. Bu yalnızca bir örnek olduğundan, bir çağrıyı başlatma, yanıtlama ve sonlandırma benzetimini yapmak için kullanılan üç yöntem vardır.
StartCallRequest sınıfı
StartCallRequest
Statik sınıfı, giden çağrılarla çalışırken kullanılacak birkaç yardımcı yöntem sağlar:
using System;
using Foundation;
using Intents;
namespace MonkeyCall
{
public static class StartCallRequest
{
public static string URLScheme {
get { return "monkeycall"; }
}
public static string ActivityType {
get { return INIntentIdentifier.StartAudioCall.GetConstant ().ToString (); }
}
public static string CallHandleFromURL (NSUrl url)
{
// Is this a MonkeyCall handle?
if (url.Scheme == URLScheme) {
// Yes, return host
return url.Host;
} else {
// Not handled
return null;
}
}
public static string CallHandleFromActivity (NSUserActivity activity)
{
// Is this a start call activity?
if (activity.ActivityType == ActivityType) {
// Yes, trap any errors
try {
// Get first contact
var interaction = activity.GetInteraction ();
var startAudioCallIntent = interaction.Intent as INStartAudioCallIntent;
var contact = startAudioCallIntent.Contacts [0];
// Get the person handle
return contact.PersonHandle.Value;
} catch {
// Error, report null
return null;
}
} else {
// Not handled
return null;
}
}
}
}
CallHandleFromURL
ve CallHandleFromActivity
sınıfları, giden aramada çağrılan kişinin kişi tanıtıcısını almak için AppDelegate içinde kullanılır. Daha fazla bilgi için lütfen aşağıdaki Giden Çağrıları İşleme bölümüne bakın.
ActiveCallManager sınıfı
sınıfı MonkeyCall ActiveCallManager
uygulamasındaki tüm açık çağrıları işler.
using System;
using System.Collections.Generic;
using Foundation;
using CallKit;
namespace MonkeyCall
{
public class ActiveCallManager
{
#region Private Variables
private CXCallController CallController = new CXCallController ();
#endregion
#region Computed Properties
public List<ActiveCall> Calls { get; set; }
#endregion
#region Constructors
public ActiveCallManager ()
{
// Initialize
this.Calls = new List<ActiveCall> ();
}
#endregion
#region Private Methods
private void SendTransactionRequest (CXTransaction transaction)
{
// Send request to call controller
CallController.RequestTransaction (transaction, (error) => {
// Was there an error?
if (error == null) {
// No, report success
Console.WriteLine ("Transaction request sent successfully.");
} else {
// Yes, report error
Console.WriteLine ("Error requesting transaction: {0}", error);
}
});
}
#endregion
#region Public Methods
public ActiveCall FindCall (NSUuid uuid)
{
// Scan for requested call
foreach (ActiveCall call in Calls) {
if (call.UUID.Equals(uuid)) return call;
}
// Not found
return null;
}
public void StartCall (string contact)
{
// Build call action
var handle = new CXHandle (CXHandleType.Generic, contact);
var startCallAction = new CXStartCallAction (new NSUuid (), handle);
// Create transaction
var transaction = new CXTransaction (startCallAction);
// Inform system of call request
SendTransactionRequest (transaction);
}
public void EndCall (ActiveCall call)
{
// Build action
var endCallAction = new CXEndCallAction (call.UUID);
// Create transaction
var transaction = new CXTransaction (endCallAction);
// Inform system of call request
SendTransactionRequest (transaction);
}
public void PlaceCallOnHold (ActiveCall call)
{
// Build action
var holdCallAction = new CXSetHeldCallAction (call.UUID, true);
// Create transaction
var transaction = new CXTransaction (holdCallAction);
// Inform system of call request
SendTransactionRequest (transaction);
}
public void RemoveCallFromOnHold (ActiveCall call)
{
// Build action
var holdCallAction = new CXSetHeldCallAction (call.UUID, false);
// Create transaction
var transaction = new CXTransaction (holdCallAction);
// Inform system of call request
SendTransactionRequest (transaction);
}
#endregion
}
}
Yine, bu yalnızca bir simülasyon olduğundan, ActiveCallManager
yalnızca bir nesne koleksiyonu ActiveCall
tutar ve özelliği tarafından UUID
belirli bir çağrıyı bulmak için bir yordamı vardır. Ayrıca, giden çağrının bekleme durumunu başlatma, sonlandırma ve değiştirme yöntemlerini de içerir. Daha fazla bilgi için lütfen aşağıdaki Giden Çağrıları İşleme bölümüne bakın.
ProviderDelegate sınıfı
Yukarıda açıklandığı gibi, CXProvider
bant dışı bildirimler için uygulama ile Sistem arasında iki yönlü iletişim sağlar. Geliştiricinin bir özel CXProviderDelegate
sağlaması ve bant dışı CallKit olaylarını işlemek için bunu uygulamasına eklemesi CXProvider
gerekir. MonkeyCall aşağıdakileri CXProviderDelegate
kullanır:
using System;
using Foundation;
using CallKit;
using UIKit;
namespace MonkeyCall
{
public class ProviderDelegate : CXProviderDelegate
{
#region Computed Properties
public ActiveCallManager CallManager { get; set;}
public CXProviderConfiguration Configuration { get; set; }
public CXProvider Provider { get; set; }
#endregion
#region Constructors
public ProviderDelegate (ActiveCallManager callManager)
{
// Save connection to call manager
CallManager = callManager;
// Define handle types
var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };
// Get Image Template
var templateImage = UIImage.FromFile ("telephone_receiver.png");
// Setup the initial configurations
Configuration = new CXProviderConfiguration ("MonkeyCall") {
MaximumCallsPerCallGroup = 1,
SupportedHandleTypes = new NSSet<NSNumber> (handleTypes),
IconTemplateImageData = templateImage.AsPNG(),
RingtoneSound = "musicloop01.wav"
};
// Create a new provider
Provider = new CXProvider (Configuration);
// Attach this delegate
Provider.SetDelegate (this, null);
}
#endregion
#region Override Methods
public override void DidReset (CXProvider provider)
{
// Remove all calls
CallManager.Calls.Clear ();
}
public override void PerformStartCallAction (CXProvider provider, CXStartCallAction action)
{
// Create new call record
var activeCall = new ActiveCall (action.CallUuid, action.CallHandle.Value, true);
// Monitor state changes
activeCall.StartingConnectionChanged += (call) => {
if (call.isConnecting) {
// Inform system that the call is starting
Provider.ReportConnectingOutgoingCall (call.UUID, call.StartedConnectingOn.ToNSDate());
}
};
activeCall.ConnectedChanged += (call) => {
if (call.isConnected) {
// Inform system that the call has connected
provider.ReportConnectedOutgoingCall (call.UUID, call.ConnectedOn.ToNSDate ());
}
};
// Start call
activeCall.StartCall ((successful) => {
// Was the call able to be started?
if (successful) {
// Yes, inform the system
action.Fulfill ();
// Add call to manager
CallManager.Calls.Add (activeCall);
} else {
// No, inform system
action.Fail ();
}
});
}
public override void PerformAnswerCallAction (CXProvider provider, CXAnswerCallAction action)
{
// Find requested call
var call = CallManager.FindCall (action.CallUuid);
// Found?
if (call == null) {
// No, inform system and exit
action.Fail ();
return;
}
// Attempt to answer call
call.AnswerCall ((successful) => {
// Was the call successfully answered?
if (successful) {
// Yes, inform system
action.Fulfill ();
} else {
// No, inform system
action.Fail ();
}
});
}
public override void PerformEndCallAction (CXProvider provider, CXEndCallAction action)
{
// Find requested call
var call = CallManager.FindCall (action.CallUuid);
// Found?
if (call == null) {
// No, inform system and exit
action.Fail ();
return;
}
// Attempt to answer call
call.EndCall ((successful) => {
// Was the call successfully answered?
if (successful) {
// Remove call from manager's queue
CallManager.Calls.Remove (call);
// Yes, inform system
action.Fulfill ();
} else {
// No, inform system
action.Fail ();
}
});
}
public override void PerformSetHeldCallAction (CXProvider provider, CXSetHeldCallAction action)
{
// Find requested call
var call = CallManager.FindCall (action.CallUuid);
// Found?
if (call == null) {
// No, inform system and exit
action.Fail ();
return;
}
// Update hold status
call.isOnHold = action.OnHold;
// Inform system of success
action.Fulfill ();
}
public override void TimedOutPerformingAction (CXProvider provider, CXAction action)
{
// Inform user that the action has timed out
}
public override void DidActivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// Start the calls audio session here
}
public override void DidDeactivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// End the calls audio session and restart any non-call
// related audio
}
#endregion
#region Public Methods
public void ReportIncomingCall (NSUuid uuid, string handle)
{
// Create update to describe the incoming call and caller
var update = new CXCallUpdate ();
update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);
// Report incoming call to system
Provider.ReportNewIncomingCall (uuid, update, (error) => {
// Was the call accepted
if (error == null) {
// Yes, report to call manager
CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
} else {
// Report error to user here
Console.WriteLine ("Error: {0}", error);
}
});
}
#endregion
}
}
Bu temsilcinin bir örneği oluşturulduğunda, herhangi bir çağrı etkinliğini işlemek için kullanacağı değeri geçirilir ActiveCallManager
. Ardından, öğesinin yanıt vereceği tanıtıcı türlerini (CXHandleType
) CXProvider
tanımlar:
// Define handle types
var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };
Ayrıca, bir arama devam ederken uygulamanın simgesine uygulanacak şablon görüntüsünü alır:
// Get Image Template
var templateImage = UIImage.FromFile ("telephone_receiver.png");
Bu değerler, öğesini yapılandırmak CXProvider
için kullanılacak bir CXProviderConfiguration
içinde paketlenir:
// Setup the initial configurations
Configuration = new CXProviderConfiguration ("MonkeyCall") {
MaximumCallsPerCallGroup = 1,
SupportedHandleTypes = new NSSet<NSNumber> (handleTypes),
IconTemplateImageData = templateImage.AsPNG(),
RingtoneSound = "musicloop01.wav"
};
Temsilci daha sonra bu yapılandırmalarla yeni CXProvider
bir oluşturur ve kendisini buna ekler:
// Create a new provider
Provider = new CXProvider (Configuration);
// Attach this delegate
Provider.SetDelegate (this, null);
CallKit kullanırken, uygulama artık kendi ses oturumlarını oluşturmaz ve işlemez, bunun yerine Sistemin kendisi için oluşturup işleyecek bir ses oturumu yapılandırması ve kullanması gerekir.
Bu gerçek bir uygulama olsaydı, çağrıyı DidActivateAudioSession
Sistem tarafından sağlanan önceden yapılandırılmış AVAudioSession
bir şekilde başlatmak için yöntemi kullanılırdı:
public override void DidActivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// Start the call's audio session here...
}
Sistem tarafından sağlanan ses oturumuyla bağlantısını sonlandırmak ve serbest bırakmak için yöntemini de kullanır DidDeactivateAudioSession
:
public override void DidDeactivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// End the calls audio session and restart any non-call
// releated audio
}
Kodun geri kalanı, izleyen bölümlerde ayrıntılı olarak ele alınacaktır.
AppDelegate sınıfı
MonkeyCall, appDelegate'i kullanarak uygulama genelinde kullanılacak ve CXProviderDelegate
örneklerini ActiveCallManager
barındırıyor:
using Foundation;
using UIKit;
using Intents;
using System;
namespace MonkeyCall
{
[Register ("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
#region Constructors
public override UIWindow Window { get; set; }
public ActiveCallManager CallManager { get; set; }
public ProviderDelegate CallProviderDelegate { get; set; }
#endregion
#region Override Methods
public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
// Initialize the call handlers
CallManager = new ActiveCallManager ();
CallProviderDelegate = new ProviderDelegate (CallManager);
return true;
}
public override bool OpenUrl (UIApplication app, NSUrl url, NSDictionary options)
{
// Get handle from url
var handle = StartCallRequest.CallHandleFromURL (url);
// Found?
if (handle == null) {
// No, report to system
Console.WriteLine ("Unable to get call handle from URL: {0}", url);
return false;
} else {
// Yes, start call and inform system
CallManager.StartCall (handle);
return true;
}
}
public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
var handle = StartCallRequest.CallHandleFromActivity (userActivity);
// Found?
if (handle == null) {
// No, report to system
Console.WriteLine ("Unable to get call handle from User Activity: {0}", userActivity);
return false;
} else {
// Yes, start call and inform system
CallManager.StartCall (handle);
return true;
}
}
...
#endregion
}
}
OpenUrl
ve ContinueUserActivity
geçersiz kılma yöntemleri, uygulama giden çağrıyı işlerken kullanılır. Daha fazla bilgi için lütfen aşağıdaki Giden Çağrıları İşleme bölümüne bakın.
Gelen çağrıları işleme
Gelen VOIP çağrısının tipik bir gelen arama iş akışı sırasında geçebileceği çeşitli durumlar ve işlemler vardır:
- Kullanıcıya (ve Sisteme) gelen bir çağrının mevcut olduğunu bildirme.
- Kullanıcı aramayı yanıtlamak istediğinde bildirim alma ve aramayı diğer kullanıcıyla başlatma.
- Kullanıcı geçerli çağrıyı sonlandırmak istediğinde Sistemi ve İletişim Ağını bilgilendirin.
Aşağıdaki bölümlerde, bir uygulamanın bir örnek olarak MonkeyCall VOIP uygulamasını kullanarak gelen arama iş akışını işlemek için CallKit'i nasıl kullanabileceğine ayrıntılı bir bakış sunulur.
Kullanıcıyı gelen arama hakkında bilgilendirme
Uzak kullanıcı yerel kullanıcıyla VOIP konuşması başlattığında aşağıdakiler gerçekleşir:
- Uygulama, İletişim Ağı'ndan gelen bir VOIP çağrısı olduğuna dair bir bildirim alır.
- Uygulama, çağrısı hakkında
CXProvider
bilgilendirmek üzere Sisteme birCXCallUpdate
göndermek için öğesini kullanır. - Sistem çağrısını CallKit kullanarak Sistem kullanıcı arabirimine, Sistem Hizmetleri'ne ve diğer VOIP uygulamalarına yayımlar.
Örneğin, içinde CXProviderDelegate
:
public void ReportIncomingCall (NSUuid uuid, string handle)
{
// Create update to describe the incoming call and caller
var update = new CXCallUpdate ();
update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);
// Report incoming call to system
Provider.ReportNewIncomingCall (uuid, update, (error) => {
// Was the call accepted
if (error == null) {
// Yes, report to call manager
CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
} else {
// Report error to user here
Console.WriteLine ("Error: {0}", error);
}
});
}
Bu kod yeni CXCallUpdate
bir örnek oluşturur ve çağıranı tanımlayan bir tanıtıcı ekler. Ardından, çağrının sistemini bilgilendirmek için sınıfının yöntemini CXProvider
kullanırReportNewIncomingCall
. Başarılı olursa, çağrı uygulamanın etkin çağrı koleksiyonuna eklenir, aksi takdirde hatanın kullanıcıya bildirilmesi gerekir.
Gelen aramayı yanıtlayan kullanıcı
Kullanıcı gelen VOIP çağrısını yanıtlamak isterse aşağıdakiler gerçekleşir:
- Sistem kullanıcı arabirimi, kullanıcının VOIP çağrısını yanıtlamak istediğini Sisteme bildirir.
- Sistem, uygulamaya
CXProvider
Yanıt Amacı hakkında bilgi veren birCXAnswerCallAction
gönderir. - Uygulama, İletişim Ağı'na kullanıcının aramayı yanıtladığını bildirir ve VOIP çağrısı her zamanki gibi devam eder.
Örneğin, içinde CXProviderDelegate
:
public override void PerformAnswerCallAction (CXProvider provider, CXAnswerCallAction action)
{
// Find requested call
var call = CallManager.FindCall (action.CallUuid);
// Found?
if (call == null) {
// No, inform system and exit
action.Fail ();
return;
}
// Attempt to answer call
call.AnswerCall ((successful) => {
// Was the call successfully answered?
if (successful) {
// Yes, inform system
action.Fulfill ();
} else {
// No, inform system
action.Fail ();
}
});
}
Bu kod ilk olarak etkin çağrı listesinde verilen çağrıyı arar. Çağrı bulunamazsa sisteme bildirim gönderilir ve yöntemden çıkılır. Bulunursa, AnswerCall
çağrıyı ActiveCall
başlatmak için sınıfının yöntemi çağrılır ve sistem başarılı veya başarısız olursa bilgidir.
Gelen aramayı sonlandıran kullanıcı
Kullanıcı, uygulamanın kullanıcı arabiriminden çağrıyı sonlandırmak isterse aşağıdakiler gerçekleşir:
- Uygulama, çağrının sona erdiğini bildirmek için Sisteme gönderilen bir
CXTransaction
içinde paketlenen bir oluştururCXEndCallAction
. - Sistem, Arama Sonlandırma Amacını doğrular ve aracılığıyla
CXProvider
uygulamaya geri gönderirCXEndCallAction
. - Ardından uygulama, İletişim Ağı'na çağrının sona erdiğini bildirir.
Örneğin, içinde CXProviderDelegate
:
public override void PerformEndCallAction (CXProvider provider, CXEndCallAction action)
{
// Find requested call
var call = CallManager.FindCall (action.CallUuid);
// Found?
if (call == null) {
// No, inform system and exit
action.Fail ();
return;
}
// Attempt to answer call
call.EndCall ((successful) => {
// Was the call successfully answered?
if (successful) {
// Remove call from manager's queue
CallManager.Calls.Remove (call);
// Yes, inform system
action.Fulfill ();
} else {
// No, inform system
action.Fail ();
}
});
}
Bu kod ilk olarak etkin çağrı listesinde verilen çağrıyı arar. Çağrı bulunamazsa sisteme bildirim gönderilir ve yöntemden çıkılır. Bulunursa, EndCall
çağrıyı ActiveCall
sonlandırmak için sınıfının yöntemi çağrılır ve sistem başarılı veya başarısız olursa bilgidir. Başarılı olursa, çağrı etkin çağrılar koleksiyonundan kaldırılır.
Birden çok çağrıyı yönetme
Çoğu VOIP uygulaması aynı anda birden çok çağrıyı işleyebilir. Örneğin, şu anda etkin bir VOIP araması varsa ve uygulama yeni bir gelen arama olduğuna ilişkin bildirim alıyorsa, kullanıcı ikinci aramayı yanıtlamak için ilk aramada duraklatabilir veya kapatabilir.
Yukarıda belirtilen durumda, Sistem uygulamaya birden çok eylemin (ve CXAnswerCallAction
gibiCXEndCallAction
) bir listesini içeren bir gönderirCXTransaction
. Sistemin kullanıcı arabirimini uygun şekilde güncelleştirebilmesi için bu eylemlerin tümünün tek tek yerine getirilmesi gerekir.
Giden çağrıları işleme
Kullanıcı Son Kullanılanlar listesinden (Telefon uygulamasında) bir girişe dokunursa( örneğin, uygulamaya ait bir çağrıdan), sistem tarafından bir Arama Başlatma Amacı gönderilir:
- Uygulama, Sistemden aldığı Çağrıyı Başlatma Amacını temel alan bir Arama Başlatma Eylemi oluşturur.
- Uygulama, sistemden Çağrı Başlatma Eylemini istemek için öğesini kullanır
CXCallController
. - Sistem Eylemi kabul ederse, temsilci aracılığıyla
XCProvider
uygulamaya döndürülür. - Uygulama giden çağrıyı İletişim Ağı ile başlatır.
Amaçlar hakkında daha fazla bilgi için lütfen Intents and Intents UI Extensions belgelerimize bakın.
Giden arama yaşam döngüsü
CallKit ve giden bir çağrıyla çalışırken uygulamanın sistemi aşağıdaki yaşam döngüsü olayları hakkında bilgilendirmesi gerekir:
- Başlatılıyor - Giden bir çağrının başlamak üzere olduğunu sisteme bildirin.
- Başlatıldı - Giden bir çağrının başlatıldığını sisteme bildirin.
- Bağlan ing - Giden çağrının bağlandığını sisteme bildirin.
- Bağlan- Giden aramanın bağlandığını ve her iki tarafın da şimdi konuşabileceğini bildirin.
Örneğin, aşağıdaki kod giden bir çağrı başlatır:
private CXCallController CallController = new CXCallController ();
...
private void SendTransactionRequest (CXTransaction transaction)
{
// Send request to call controller
CallController.RequestTransaction (transaction, (error) => {
// Was there an error?
if (error == null) {
// No, report success
Console.WriteLine ("Transaction request sent successfully.");
} else {
// Yes, report error
Console.WriteLine ("Error requesting transaction: {0}", error);
}
});
}
public void StartCall (string contact)
{
// Build call action
var handle = new CXHandle (CXHandleType.Generic, contact);
var startCallAction = new CXStartCallAction (new NSUuid (), handle);
// Create transaction
var transaction = new CXTransaction (startCallAction);
// Inform system of call request
SendTransactionRequest (transaction);
}
bir oluşturur CXHandle
ve sınıfı yöntemini CXCallController
kullanarak RequestTransaction
Sisteme gönderilen bir içine paketlenmiş bir CXTransaction
yapılandırmak CXStartCallAction
için kullanır. Sistem, yöntemini çağırarakRequestTransaction
, yeni çağrı başlamadan önce kaynak (Telefon uygulaması, FaceTime, VOIP vb.) fark etmeden mevcut aramaları beklemeye alabilir.
Giden VOIP araması başlatma isteği Siri, Kişi kartındaki bir giriş (Kişiler uygulamasında) veya Son Kullanılanlar listesinden (Telefon uygulamasında) gibi birkaç farklı kaynaktan gelebilir. Bu durumlarda, uygulamaya içinde NSUserActivity
bir Çağrı Başlatma Amacı gönderilir ve AppDelegate'in bunu işlemesi gerekir:
public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
var handle = StartCallRequest.CallHandleFromActivity (userActivity);
// Found?
if (handle == null) {
// No, report to system
Console.WriteLine ("Unable to get call handle from User Activity: {0}", userActivity);
return false;
} else {
// Yes, start call and inform system
CallManager.StartCall (handle);
return true;
}
}
CallHandleFromActivity
Burada yardımcı sınıfının StartCallRequest
yöntemi, tanıtıcıyı çağrılan kişiye almak için kullanılır (yukarıdaki StartCallRequest Sınıfına bakın).
PerformStartCallAction
ProviderDelegate Sınıfı yöntemi, son olarak gerçek giden çağrıyı başlatmak ve Sistem'e yaşam döngüsünü bildirmek için kullanılır:
public override void PerformStartCallAction (CXProvider provider, CXStartCallAction action)
{
// Create new call record
var activeCall = new ActiveCall (action.CallUuid, action.CallHandle.Value, true);
// Monitor state changes
activeCall.StartingConnectionChanged += (call) => {
if (call.IsConnecting) {
// Inform system that the call is starting
Provider.ReportConnectingOutgoingCall (call.UUID, call.StartedConnectingOn.ToNSDate());
}
};
activeCall.ConnectedChanged += (call) => {
if (call.IsConnected) {
// Inform system that the call has connected
Provider.ReportConnectedOutgoingCall (call.UUID, call.ConnectedOn.ToNSDate ());
}
};
// Start call
activeCall.StartCall ((successful) => {
// Was the call able to be started?
if (successful) {
// Yes, inform the system
action.Fulfill ();
// Add call to manager
CallManager.Calls.Add (activeCall);
} else {
// No, inform system
action.Fail ();
}
});
}
Sınıfının bir örneğini ActiveCall
oluşturur (devam eden çağrı hakkındaki bilgileri tutmak için) ve çağrılan kişiyle doldurulur. StartingConnectionChanged
ve ConnectedChanged
olayları, giden arama yaşam döngüsünü izlemek ve raporlamak için kullanılır. Çağrı başlatılır ve Sistem Eylemin yerine getirildiğini bildirir.
Giden aramayı sonlandırma
Kullanıcı giden bir çağrıyı bitirdiğinde ve aramayı sonlandırmak istediğinde aşağıdaki kod kullanılabilir:
private CXCallController CallController = new CXCallController ();
...
private void SendTransactionRequest (CXTransaction transaction)
{
// Send request to call controller
CallController.RequestTransaction (transaction, (error) => {
// Was there an error?
if (error == null) {
// No, report success
Console.WriteLine ("Transaction request sent successfully.");
} else {
// Yes, report error
Console.WriteLine ("Error requesting transaction: {0}", error);
}
});
}
public void EndCall (ActiveCall call)
{
// Build action
var endCallAction = new CXEndCallAction (call.UUID);
// Create transaction
var transaction = new CXTransaction (endCallAction);
// Inform system of call request
SendTransactionRequest (transaction);
}
Çağrısının UUID değeriyle uçtan uca bir CXEndCallAction
oluşturursa, sınıfı yöntemini CXCallController
kullanarak RequestTransaction
Sisteme gönderilen bir CXTransaction
içinde paketler.
Ek CallKit ayrıntıları
Bu bölümde, geliştiricinin CallKit ile çalışırken dikkate alması gereken bazı ek ayrıntılar ele alınacaktır:
- Sağlayıcı Yapılandırması
- Eylem Hataları
- Sistem Kısıtlamaları
- VOIP Ses
Sağlayıcı yapılandırması
Sağlayıcı yapılandırması, iOS 10 VOIP uygulamasının CallKit ile çalışırken kullanıcı deneyimini (yerel Arama İçi Kullanıcı Arabirimi içinde) özelleştirmesine olanak tanır.
Bir uygulama aşağıdaki özelleştirme türlerini yapabilir:
- Yerelleştirilmiş bir ad görüntüleme.
- Görüntülü arama desteğini etkinleştirin.
- Kendi şablon görüntüsü simgesini sunarak Arama İçi kullanıcı arabirimindeki düğmeleri özelleştirin. Özel düğmelerle kullanıcı etkileşimi doğrudan işlenecek uygulamaya gönderilir.
Eylem hataları
CallKit kullanan iOS 10 VOIP uygulamalarının, başarısız olan Eylemleri düzgün bir şekilde işlemesi ve kullanıcıyı Eylem durumu hakkında her zaman bilgilendirmesi gerekir.
Aşağıdaki örneği dikkate alın:
- Uygulama bir Arama Başlatma Eylemi aldı ve İletişim Ağı ile yeni bir VOIP çağrısı başlatma işlemine başladı.
- Ağ iletişiminin sınırlı olması veya olmaması nedeniyle bu bağlantı başarısız olur.
- Uygulamanın, Sistemi hatayla ilgili bilgilendirmek için Yeniden Başlat Çağrısı Eylemine (
Action.Fail()
) Başarısız iletisi göndermesi gerekir. - Bu, Sistemin kullanıcıya çağrının durumunu bildirmesini sağlar. Örneğin, Arama Hatası kullanıcı arabirimini görüntülemek için.
Ayrıca, bir iOS 10 VOIP uygulamasının, beklenen bir Eylemin belirli bir süre içinde işlenememesi durumunda oluşabilecek Zaman Aşımı Hatalarına yanıt vermesi gerekir. CallKit tarafından sağlanan her Eylem Türünün kendisiyle ilişkilendirilmiş en fazla Zaman Aşımı değeri vardır. Bu Zaman Aşımı değerleri, kullanıcı tarafından istenen Tüm CallKit Eyleminin duyarlı bir şekilde işlenmesini sağlar, böylece işletim sistemi akıcı ve hızlı yanıt verir.
Sağlayıcı Temsilcisinde (CXProviderDelegate
) bu Zaman Aşımı durumlarını düzgün bir şekilde işlemek için geçersiz kılınması gereken çeşitli yöntemler vardır.
Sistem kısıtlamaları
iOS 10 VOIP uygulamasını çalıştıran iOS cihazının geçerli durumuna bağlı olarak, belirli sistem kısıtlamaları uygulanabilir.
Örneğin, gelen VOIP çağrısı şu durumlarda Sistem tarafından kısıtlanabilir:
- Arayan kişi, kullanıcının Engellenen Arayan Listesi'ndedir.
- Kullanıcının iOS cihazı Rahatsız Etmeyin modundadır.
VoIP çağrısı bu durumlardan herhangi biri tarafından kısıtlanmışsa, bunu işlemek için aşağıdaki kodu kullanın:
public class ProviderDelegate : CXProviderDelegate
{
...
public void ReportIncomingCall (NSUuid uuid, string handle)
{
// Create update to describe the incoming call and caller
var update = new CXCallUpdate ();
update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);
// Report incoming call to system
Provider.ReportNewIncomingCall (uuid, update, (error) => {
// Was the call accepted
if (error == null) {
// Yes, report to call manager
CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
} else {
// Report error to user here
if (error.Code == (int)CXErrorCodeIncomingCallError.CallUuidAlreadyExists) {
// Handle duplicate call ID
} else if (error.Code == (int)CXErrorCodeIncomingCallError.FilteredByBlockList) {
// Handle call from blocked user
} else if (error.Code == (int)CXErrorCodeIncomingCallError.FilteredByDoNotDisturb) {
// Handle call while in do-not-disturb mode
} else {
// Handle unknown error
}
}
});
}
}
VOIP sesi
CallKit, bir iOS 10 VOIP uygulamasının canlı VOIP araması sırasında gerektireceği ses kaynaklarını işlemek için çeşitli avantajlar sağlar. En büyük avantajlardan biri, iOS 10'da çalışırken uygulamanın ses oturumunun yükseltilmiş öncelikleri olmasıdır. Bu, yerleşik Telefon ve FaceTime uygulamalarıyla aynı öncelik düzeyidir ve bu gelişmiş öncelik düzeyi, çalışan diğer uygulamaların VOIP uygulamasının ses oturumunu kesintiye uğratmasını engeller.
Buna ek olarak CallKit, kullanıcı tercihlerine ve cihaz durumlarına göre canlı arama sırasında performansı geliştirebilen ve VOIP sesini belirli çıkış cihazlarına akıllı bir şekilde yönlendirebilen diğer ses yönlendirme ipuçlarına erişebilir. Örneğin, bluetooth kulaklık, canlı CarPlay bağlantısı veya Erişilebilirlik ayarları gibi bağlı cihazlara göre.
CallKit kullanan tipik bir VOIP çağrısının yaşam döngüsü sırasında uygulamanın CallKit'in sağlayacağı Ses Akışını yapılandırması gerekir. Aşağıdaki örneğe göz atın:
- Gelen aramayı yanıtlamak için uygulama tarafından Bir Arama Başlat Eylemi alınır.
- Bu Eylem uygulama tarafından yerine getirilmeden önce, uygulaması için gereken yapılandırmayı
AVAudioSession
sağlar. - Uygulama, Eylemin yerine getirildiğini Sisteme bildirir.
- Çağrı bağlanmadan önce CallKit, uygulamanın istediği yapılandırmayla eşleşen yüksek öncelikli
AVAudioSession
bir özellik sağlar. Uygulama, yöntemiyleDidActivateAudioSession
CXProviderDelegate
bildirilir.
Arama dizini uzantılarıyla çalışma
CallKit ile çalışırken, Arama Dizini Uzantıları iOS cihazındaki Kişi uygulamasındaki kişilere engellenen arama numaraları eklemenin ve belirli bir VOIP uygulamasına özgü numaraları tanımlamanın bir yolunu sağlar.
Arama dizini uzantısı uygulama
Xamarin.iOS uygulamasında Çağrı Dizini Uzantısı uygulamak için aşağıdakileri yapın:
Uygulamanın çözümünü Mac için Visual Studio açın.
Çözüm Gezgini Çözüm Adı'na sağ tıklayın ve Yeni Proje Ekle'yi>seçin.
iOS>Uzantıları Dizin Uzantılarını> Çağır'ı seçin ve İleri düğmesine tıklayın:
Uzantı için bir Ad girin ve İleri düğmesine tıklayın:
Gerekirse Proje Adını ve/veya Çözüm Adını ayarlayın ve Oluştur düğmesine tıklayın:
Bu, projeye aşağıdakine benzer bir CallDirectoryHandler.cs
sınıf ekler:
using System;
using Foundation;
using CallKit;
namespace MonkeyCallDirExtension
{
[Register ("CallDirectoryHandler")]
public class CallDirectoryHandler : CXCallDirectoryProvider, ICXCallDirectoryExtensionContextDelegate
{
#region Constructors
protected CallDirectoryHandler (IntPtr handle) : base (handle)
{
// Note: this .ctor should not contain any initialization logic.
}
#endregion
#region Override Methods
public override void BeginRequest (CXCallDirectoryExtensionContext context)
{
context.Delegate = this;
if (!AddBlockingPhoneNumbers (context)) {
Console.WriteLine ("Unable to add blocking phone numbers");
var error = new NSError (new NSString ("CallDirectoryHandler"), 1, null);
context.CancelRequest (error);
return;
}
if (!AddIdentificationPhoneNumbers (context)) {
Console.WriteLine ("Unable to add identification phone numbers");
var error = new NSError (new NSString ("CallDirectoryHandler"), 2, null);
context.CancelRequest (error);
return;
}
context.CompleteRequest (null);
}
#endregion
#region Private Methods
private bool AddBlockingPhoneNumbers (CXCallDirectoryExtensionContext context)
{
// Retrieve phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
// consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
//
// Numbers must be provided in numerically ascending order.
long [] phoneNumbers = { 14085555555, 18005555555 };
foreach (var phoneNumber in phoneNumbers)
context.AddBlockingEntry (phoneNumber);
return true;
}
private bool AddIdentificationPhoneNumbers (CXCallDirectoryExtensionContext context)
{
// Retrieve phone numbers to identify and their identification labels from data store. For optimal performance and memory usage when there are many phone numbers,
// consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
//
// Numbers must be provided in numerically ascending order.
long [] phoneNumbers = { 18775555555, 18885555555 };
string [] labels = { "Telemarketer", "Local business" };
for (var i = 0; i < phoneNumbers.Length; i++) {
long phoneNumber = phoneNumbers [i];
string label = labels [i];
context.AddIdentificationEntry (phoneNumber, label);
}
return true;
}
#endregion
#region Public Methods
public void RequestFailed (CXCallDirectoryExtensionContext extensionContext, NSError error)
{
// An error occurred while adding blocking or identification entries, check the NSError for details.
// For Call Directory error codes, see the CXErrorCodeCallDirectoryManagerError enum.
//
// This may be used to store the error details in a location accessible by the extension's containing app, so that the
// app may be notified about errors which occurred while loading data even if the request to load data was initiated by
// the user in Settings instead of via the app itself.
}
#endregion
}
}
BeginRequest
Çağrı Dizin İşleyicisi'ndeki yöntemin gerekli işlevselliği sağlamak için değiştirilmesi gerekir. Yukarıdaki örnekte VOIP uygulamasının kişiler veritabanında engellenen ve kullanılabilir sayıların listesini ayarlamaya çalışır. herhangi bir nedenle isteklerden biri başarısız olursa, hatayı açıklamak için bir NSError
oluşturun ve sınıfın CancelRequest
CXCallDirectoryExtensionContext
yöntemini geçirin.
Engellenen sayıları ayarlamak için sınıfının yöntemini CXCallDirectoryExtensionContext
kullanınAddBlockingEntry
. yöntemine sağlanan sayılar sayısal olarak artan sırada olmalıdır . Çok sayıda telefon numarası olduğunda en iyi performans ve bellek kullanımı için, belirli bir zamanda yalnızca bir sayı alt kümesini yüklemeyi ve yüklenen her numara toplu işlemi sırasında ayrılan nesneleri serbest bırakmak için otomatik sürüm havuzlarını kullanmayı göz önünde bulundurun.
VOIP uygulaması tarafından bilinen kişi numaralarını Kişi uygulamasına bildirmek için sınıfının yöntemini CXCallDirectoryExtensionContext
kullanın AddIdentificationEntry
ve hem sayıyı hem de tanımlayıcı etiketi sağlayın. Yine yönteme sağlanan sayıların sayısal olarak artan sırada olması gerekir . Çok sayıda telefon numarası olduğunda en iyi performans ve bellek kullanımı için, belirli bir zamanda yalnızca bir sayı alt kümesini yüklemeyi ve yüklenen her numara toplu işlemi sırasında ayrılan nesneleri serbest bırakmak için otomatik sürüm havuzlarını kullanmayı göz önünde bulundurun.
Özet
Bu makalede, Apple'ın iOS 10'da yayımladığı yeni CallKit API'si ve Xamarin.iOS VOIP uygulamalarında nasıl uygulandığı ele alınmıştır. CallKit'in bir uygulamanın iOS Sistemi ile tümleştirilmesine nasıl izin verdiği, yerleşik uygulamalarla (Telefon gibi) özellik eşliği sağlama şekli ve Siri etkileşimleri ve Kişiler uygulamaları aracılığıyla Kilit ve Giriş Ekranları gibi konumlarda iOS genelinde uygulamanın görünürlüğünü nasıl artırdığını göstermiştir.