Xamarin.iOS'ta CallKit
iOS 10'daki yeni CallKit API'si, VOIP uygulamalarının iPhone arabirimiyle tümleştirip son kullanıcıya tanıdık bir arabirim ve deneyim sağlamasını sağlar. Bu API ile kullanıcılar iOS cihazın Kilit Ekranından VOIP çağrılarını görüntüleyip etkileşimde bulunarak, Telefon 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 1. taraf deneyimine yükseltecek yeni bir çerçevedir. CallKit API'si, VOIP uygulamalarının iPhone arabirimiyle tümleştirilmelerini ve son kullanıcıya tanıdık bir arabirim ve deneyim sağlamalarını sağlar. Yerleşik Telefon uygulamasında olduğu gibi, bir kullanıcı iOS cihazın Kilit Ekranından VOIP çağrılarını görüntüleyip etkileşimde bulunarak 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 (Arayan Kimliği) ilişkilendirilen veya bir numaranın ne zaman engellenmesi gerektiğini (Çağrı Engellemesi) sisteme anlatan Uygulama Uzantıları oluşturma olanağı sağlar.
Mevcut VOIP uygulaması deneyimi
Yeni CallKit API'sini ve becerilerini tartışmadan önce, iOS 9'da 3. taraf VOIP uygulamasıyla (ve daha küçüktür) Ve daha küçük bir kullanıcı deneyimine göz atarak, MaymunCall adlı kurgusal BIR VOIP uygulamasını kullanın. MaymunCall, kullanıcının mevcut iOS API'lerini kullanarak VOIP çağrıları göndermesini ve almalarını sağlayan basit bir uygulamadır.
Şu anda kullanıcı, MaymunCall'ta gelen bir çağrı alıyorsa ve kullanıcının iPhone kilitliyse, Kilit ekranında alınan bildirim diğer bildirim türlerinden (örneğin İletiler veya Posta uygulamaları gibi) ayırt edilemez.
Kullanıcı çağrıyı yanıtlamak isterse, çağrıyı kabul etmek ve konuşmayı başlatmak için önce uygulamayı açmak ve geçiş kodunu (veya kullanıcı Touch ID'sini) girmek için MaymunCall bildirimini kaydırarak telefonu açması gerekir.
Telefon kilidi açıksa deneyim de aynı derecede zahmetli olur. Yine gelen MaymunCall çağrısı, ekranın üst kısmından gelen standart bir bildirim başlığı olarak görüntülenir. Bildirim geçici olduğu için, kullanıcı Tarafından Notification Center'ı açmaya zorlayarak ve yanıtlamak için belirli bir bildirimi bulup Ardından, MaymunCall uygulamasını el ile çağırarak veya bulup başlatmaya zorlayarak kolayca gözden kaçabilir.
CallKit VOIP uygulama deneyimi
Yeni CallKit API'lerini MaymunCall uygulamasında uygulayan kullanıcının gelen VOIP çağrısı deneyimi, iOS 10'da büyük ölçüde geliştirilebilirsiniz. Telefonu yukarıdan kilitlenirken VOIP çağrısı alan kullanıcının örneğini örnek olarak alalım. CallKit uygulanarak çağrı, tam ekran, yerel kullanıcı arabirimi ve standart yanıt verme işleviyle yerleşik Telefon uygulamasından alınan çağrı gibi iPhone'nin Kilit ekranında görünür.
Tekrarlamak gerekirse, bir MaymunCall VOIP çağrısı geldiğinde iPhone'nin kilidi açılırsa, yerleşik Telefon uygulamasının aynı tam ekran, yerel kullanıcı arabirimi ve standart çekme-cevap ve dokunma-reddetme işlevleri gösterilir ve MaymunCall özel bir ses çalma seçeneğine sahip olur.
CallKit, MaymunCall'a ek işlevsellik sağlar ve VOIP çağrılarının diğer çağrı 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 olanak sağlar, Siri'den MaymunCall çağrılarını başlatarak kullanıcıların Kişiler uygulamasındaki kullanıcılara MaymunCall çağrıları atamasına olanak sağlar.
Aşağıdaki bölümler CallKit mimarisini, gelen ve giden çağrı akışlarını ve CallKit API'sini ayrıntılı olarak ele almaktadır.
CallKit mimarisi
iOS 10'da Apple, CarPlay'de yapılan çağrıların CallKit aracılığıyla Sistem kullanıcı arabirimi tarafından bilindiği gibi tüm Sistem Hizmetlerde CallKit'i benimsedi. Aşağıdaki örnekte, MaymunCall CallKit'i benimsediği için Sistem tarafından bu yerleşik System Services ile aynı şekilde bilinir ve tüm aynı özellikleri alır:
Yukarıdaki diyagramda Yer alan MaymunCall Uygulamasına daha yakından bakın. Uygulama kendi ağıyla iletişim kurmak için tüm kodunu içerir ve kendi Kullanıcı Arabirimlerini içerir. Sistemle iletişim kurmak için CallKit'te bağlantı kurar:
CallKit'te uygulamanın kullandığı iki ana arabirim vardır:
CXProvider- Bu, MaymunCall uygulamasının sisteme meydana gelen bant dışı bildirimleri bildirmesi için izin verir.CXCallController- MaymunCall uygulamasının sisteme yerel kullanıcı eylemlerini bildirmesi için izin verir.
The CXProvider
Yukarıda belirtildiği gibi, bir uygulamanın sisteme meydana gelen bant dışı CXProvider bildirimleri bildirmesi için izin verir. Bunlar, yerel kullanıcı eylemleri nedeniyle meydana gelen ancak gelen çağrılar gibi dış olaylar nedeniyle oluşan bildirimdir.
Bir uygulama, aşağıdakiler CXProvider için kullandır:
- Sisteme gelen çağrıyı bildirme.
- Giden çağrının Sisteme bağlı olduğunu bildirme.
- Uzak kullanıcının Çağrıyı Sistem'e sonlandırarak bildirme.
Uygulama sistemle iletişim kurmak istiyorsa sınıfını kullanır ve Sistem'in uygulamayla iletişim CXCallUpdate kurması gerekirken 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ı eylemlerini sisteme bildirmesine olanak sağlar. Uygulamanın CXCallController uygulanması, sistemde diğer çağrı türleriyle etkileşime geçen bir uygulamadır. Örneğin, devam eden etkin bir telefon çağrısı zaten varsa, VOIP uygulamasının bu çağrıyı basılı tutmasına ve bir VOIP çağrısı başlatmasına veya CXCallController yanıtlamasına izin verir.
Bir uygulama, aşağıdakiler CXCallController için kullandır:
- Kullanıcının Sisteme giden çağrıyı ne zaman başlatt olduğunu raporla.
- Kullanıcı Sisteme gelen bir çağrıyı yanıtlana kadar raporla.
- Kullanıcı Sistem çağrısının sona erdiğinde raporla.
Uygulama, yerel kullanıcı eylemlerini sisteme iletirken sınıfını CXTransaction kullanır:
CallKit Uygulama
Aşağıdaki bölümlerde bir Xamarin.iOS VOIP uygulamasında CallKit'in nasıl uygulanıp uygulanmayacakları liste ve açıklama gelecektir. Örneğin, bu belge kurgusal MaymunCall VOIP uygulamasından kod kullanıyor olacaktır. Burada sunulan kod çeşitli destekleyen sınıfları temsil eder, CallKit'e özgü bölümler aşağıdaki bölümlerde ayrıntılı olarak ele arilmektedir.
ActiveCall sınıfı
sınıfı, Şu anda etkin olan bir VOIP çağrısıyla ilgili tüm bilgileri tutmak için Aşağıdaki gibi ActiveCall MaymunCall uygulaması tarafından 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ının durumunu tanımlayan çeşitli özellikleri ve çağrı durumu değiştiklerde ortaya çıkarilebilecek iki olay içerir. Bu yalnızca bir örnek olduğu için çağrıyı başlatma, yanıtlama ve sonlandırma simülasyonu yapmak için kullanılan üç yöntem vardır.
StartCallRequest sınıfı
statik StartCallRequest 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;
}
}
}
}
ve sınıfları, giden çağrıda çağrılan kişinin kişi tanıtıcısını almak CallHandleFromURLCallHandleFromActivity 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ı, ActiveCallManager MaymunCall 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 benzetim olduğu için yalnızca bir nesne koleksiyonunu bulundurmaktadır ve özelliğine göre verilen bir çağrıyı bulma ActiveCallManagerActiveCallUUID yordamına sahiptir. Ayrıca giden çağrının başlatma, bitiş ve tutma durumunu 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 da belirtildiği gibi, bant dışı bildirimler için uygulama CXProvider ile Sistem arasında çift yol iletişim sağlar. Geliştiricinin bir özel değer sağlaması ve bant dışında CallKit olaylarını CXProviderDelegateCXProvider işlemesi için uygulamaya eklemesi gerekir. MaymunCall aşağıdakini 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ı ActiveCallManager etkinliğini işlemek için kullanabileceği örneği geçirildi. Ardından, tarafından yanıt verilen tanıtıcı türlerini ( CXHandleType ) CXProvider tanımlar:
// Define handle types
var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };
Ayrıca, bir çağrı devam eden 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, yapılandırmak için CXProviderConfiguration kullanılacak bir içinde paket haline gelecektir: CXProvider
// 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 CXProvider yapılandırmalarla yeni bir oluşturur ve kendisini buna iliştirer:
// Create a new provider
Provider = new CXProvider (Configuration);
// Attach this delegate
Provider.SetDelegate (this, null);
CallKit kullanılırken uygulama artık kendi ses oturumlarını oluşturmaz ve işlemez, bunun yerine Sistem tarafından oluşturulacak ve işlen bir ses oturumu yapılandıracak ve kullanmayacak.
Bu gerçek bir uygulama olsaydı, sistem tarafından sağlanan önceden yapılandırılmış bir çağrıyı başlatmak DidActivateAudioSessionAVAudioSession için yöntemi kullanılır:
public override void DidActivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// Start the call's audio session here...
}
Ayrıca yöntemini kullanarak Sistem tarafından sağlanan ses oturumuna bağlantısını son olarak serbest DidDeactivateAudioSession bırakabilirsiniz:
public override void DidDeactivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// End the calls audio session and restart any non-call
// releated audio
}
Kodun geri kalanı, aşağıdaki bölümlerde ayrıntılı olarak ele alır.
AppDelegate sınıfı
MaymunCall, AppDelegate'i kullanarak uygulamanın her ActiveCallManager yerinde kullanılacak ve örneklerini CXProviderDelegate tutar:
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
}
}
Ve OpenUrlContinueUserActivity geçersiz kılma yöntemleri, uygulama bir 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 BIR VOIP çağrısının tipik bir gelen çağrı iş akışı sırasında geçenin çeşitli durumları ve işlemleri vardır, örneğin:
- Kullanıcıya (ve Sisteme) gelen bir çağrının var olduğunu bildirme.
- Kullanıcı çağrıyı yanıtlamak ve çağrıyı diğer kullanıcıyla başlatmayı isterse bildirim alma.
- Kullanıcı geçerli çağrıyı sona ererken Sistemi ve İletişim Ağına bildirme.
Aşağıdaki bölümlerde, örnek olarak yine MaymunCall VOIP uygulamasını kullanarak bir uygulamanın gelen çağrı iş akışını işlemek için CallKit'i nasıl kullanabileceğine ilişkin ayrıntılı bir bakış yer alır.
Kullanıcıya gelen çağrıyı bildirme
Uzak bir kullanıcı yerel kullanıcıyla BIR VOIP görüşmesi başlattı mı, aşağıdakiler gerçekleşir:
- Uygulama, İletişim Ağı'dan gelen bir VOIP çağrısı olduğunu bildirer.
- Uygulama, çağrısı
CXProviderhakkında bilgi içinCXCallUpdateSistem'e bir göndermek için kullanır. - Sistem, CallKit kullanarak çağrıyı Sistem kullanıcı arabirimine, Sistem Hizmetleri'ne ve diğer VOIP uygulamalarına yayımlar.
Örneğin, CXProviderDelegate içinde:
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 bir CXCallUpdate örnek oluşturur ve çağıranı tanımacak tanıtıcıyı buna iliştirer. Ardından, çağrı ReportNewIncomingCall sistemini bilgilendirmek CXProvider için sınıfının yöntemini kullanır. Başarılı olursa, çağrı uygulamanın etkin çağrı koleksiyonuna eklenir, eklenmezse, hatanın kullanıcıya bildiriliyor olması gerekir.
Gelen çağrıyı yanıtlayan kullanıcı
Kullanıcı gelen VOIP çağrısını yanıtlamak istiyorsa aşağıdakiler gerçekleşir:
- Sistem kullanıcı arabirimi, sisteme kullanıcının VOIP çağrısını yanıtlamak istediğini bildirmektedir.
- Sistem,
CXAnswerCallActionuygulamanın yanıt amacı hakkındaCXProviderbilgi vermek için uygulamaya bir gönderir. - Uygulama, İletişim Ağına kullanıcının çağrıyı yanıtlayarak VOIP çağrısının her zamanki gibi ilerler olduğunu bildirer.
Örneğin, CXProviderDelegate içinde:
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 önce etkin çağrı listesinde verilen çağrıyı arar. Çağrı bulunamazsa sistem bilgili olur ve yöntemden çıkar. Bulunursa, çağrıyı başlatmak için sınıfının yöntemi çağrılır ve başarılı veya başarısız olursa AnswerCallActiveCall Sistem bilgidir.
Gelen çağrıyı sonlandıran kullanıcı
Kullanıcı çağrıyı uygulamanın kullanıcı arabiriminden sonlandırmak isterse, aşağıdakiler gerçekleşir:
- Uygulama,
CXEndCallActionçağrının sona eriyor olduğunuCXTransactionbildirmek için Sisteme gönderilen bir içinde paket haline gelen bir oluşturur. - Sistem, Çağrı Amacını Doğrular ve aracılığıyla
CXEndCallActionuygulamayı uygulamaya geriCXProvidergönderir. - Uygulama daha sonra İletişim Ağına çağrının sona eriyor olduğunu bildirer.
Örneğin, CXProviderDelegate içinde:
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 önce etkin çağrı listesinde verilen çağrıyı arar. Çağrı bulunamazsa sistem bilgili olur ve yöntemden çıkar. Bulunursa, çağrıyı sona ertk için sınıfının yöntemi çağrılır ve başarılı veya başarısız olursa Sistem EndCallActiveCall bilgidir. Başarılı olursa, çağrı etkin çağrı koleksiyonundan kaldırılır.
Birden çok çağrıyı yönetme
Çoğu VOIP uygulaması aynı anda birden çok çağrıyı işebilir. Örneğin, şu anda etkin bir VOIP çağrısı varsa ve uygulama yeni bir gelen çağrı olduğunu bildirse, kullanıcı ikinci çağrıyı yanıtlamak için ilk çağrıda duraklatabilir veya kapatabilir.
Yukarıdaki durumda Sistem, birden çok eylemin (ve gibi) listesini içerecek CXTransaction uygulamaya bir CXEndCallActionCXAnswerCallAction gönderir. Sistemin kullanıcı arabirimini uygun şekilde güncelleştirene kadar tüm bu eylemlerin tek tek yerine getirilmesi gerekir.
Giden çağrıları işleme
Kullanıcı Son Bulunanlar listesinden (Telefon uygulamasında) bir girişe dokunsa (örneğin, uygulamaya ait bir çağrıdan gelen) sistem tarafından Bir Başlatma Çağrısı Amacı gönderilir:
- Uygulama, Sistemden aldığı Başlatma Çağrısı Amacını temel alan bir Başlatma Çağrısı Eylemi oluşturacak.
- Uygulama, sistemden
CXCallControllerÇağrıYı Başlat Eylemlerini talep etmek için kullanır. - Sistem Eylemi kabul ederse, temsilci aracılığıyla uygulamaya
XCProviderdöndürülür. - Uygulama giden çağrıyı İletişim Ağı ile başlatır.
Intents (Amaç) hakkında daha fazla bilgi için lütfen Intents and Intents UI Extensions (Amaç ve Amaç Kullanıcı Arabirimi Uzantıları) belgelerimize bakın.
Giden çağrı yaşam döngüsü
CallKit ve giden çağrı ile çalışırken, uygulamanın Sistem'e aşağıdaki yaşam döngüsü olaylarını bildirmesi gerekir:
- Başlangıç - Sisteme giden çağrının başlamak üzere olduğunu bildirin.
- Başlatıldı - Sisteme giden çağrının başlat olduğunu bildirme.
- Bağlanma - Sisteme giden çağrının bağlanıyor olduğunu bildirin.
- Bağlandı - Giden çağrının bağlandı ve her iki taraf da şimdi konuşasın.
Örneğin, aşağıdaki kod bir giden çağrı başlatacak:
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 ve sınıf yöntemi kullanılarak Sisteme gönderilen bir içinde paketlenmiş bir CXHandleCXStartCallAction yapılandırmak için CXTransactionRequestTransactionCXCallController kullanır. Sistem, yöntemini çağırarak, kaynak RequestTransaction (Telefon app, FaceTime, VOIP vb.) fark etmez, mevcut çağrıları yeni çağrı başlamadan önce basılı tutabilir.
Giden VOIP çağrısı başlatma isteği Siri, Kişi kartında bir giriş (Kişiler uygulamasında) veya Son Çıkanlar listesinden (Telefon uygulamasında) gibi birkaç farklı kaynaktan gelebilir. Bu durumlarda, uygulamanın içinde bir Başlangıç Çağrısı Amacı gönderilir ve NSUserActivity 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;
}
}
Burada yardımcı sınıfının yöntemi çağrılan kişiye tanıtıcıyı almak için kullanılır CallHandleFromActivityStartCallRequest (yukarıdaki CallHandleFromActivity bakın).
PerformStartCallActionPerformStartCallAction 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 oluşturur (devam eden çağrıyla ilgili bilgileri tutmak için) ve ActiveCall çağrıyı yapılan kişi ile doldurmak için. ve StartingConnectionChangedConnectedChanged olayları, giden çağrı yaşam döngüsünü izlemek ve rapor etmek için kullanılır. Çağrı başlatıldı ve Sistem Eylemin karşılandı olduğunu bildirdi.
Giden çağrıyı sonlandırma
Kullanıcı bir giden çağrıyı tamamlasa ve sona erer isterse, 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);
}
Bitiş çağrısının UUID'sinde bir oluşturursa, sınıfın yöntemi kullanılarak Sisteme gönderilen bir CXEndCallActionCXTransaction içinde RequestTransactionCXCallController paketler.
Ek CallKit ayrıntıları
Bu bölümde, geliştiricinin CallKit ile çalışırken göz önünde bulundurulması gereken bazı ek ayrıntılar yer alır, örneğin:
- 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 özelleştirmesi (yerel In-Call kullanıcı arabirimi içinde) sağlar.
Bir uygulama aşağıdaki özelleştirme türlerini gerçekleştirebilirsiniz:
- Yerelleştirilmiş bir ad görüntüler.
- Video çağrısı desteğini etkinleştirin.
- Kendi şablon görüntüsünü In-Call kullanıcı arabirimi üzerindeki 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 doğru şekilde işlemesi ve kullanıcıya her zaman Eylem durumu hakkında bilgi vermesi gerekir.
Aşağıdaki örneği göz önünde bulundurarak:
- Uygulama bir Çağrı Başlatma Eylemi aldı ve İletişim Ağı ile yeni bir VOIP çağrısı başlatma işlemini başlattı.
- Sınırlı veya hiçbir ağ iletişimi özelliği nedeniyle bu bağlantı başarısız olur.
- Uygulamanın, Sistemi hata hakkında bilgilendirmek için ÇağrıYı Başlat Eylemine ( ) Yeniden Başarısız Oldu iletisi göndermesi gerekir.
- Bu, Sistem'in kullanıcıya çağrının durumunu bildirmesi için izin verir. Örneğin, Çağrı Hatası kullanıcı arabirimini görüntülemek için.
Buna ek olarak, bir iOS 10 VOIP uygulamasının beklenen bir Eylem belirli bir süre içinde işlenemediklerinde ortaya çıkabilir Zaman Aşımı Hatalarına yanıt vermesi gerekir. CallKit tarafından sağlanan her Eylem Türü ile ilişkili en yüksek Zaman Aşımı değeri vardır. Bu Zaman Aşımı değerleri, kullanıcı tarafından istenen tüm CallKit Eyleminin yanıt veren bir şekilde işlenerek işletim sistemi akıcı ve hızlı yanıt veren bir şekilde olmasını sağlar.
Sağlayıcı Temsilcisinde ( ) bu Zaman Aşımı durumlarını da uygun bir şekilde işlemek için geçersiz CXProviderDelegate kılınacak çeşitli yöntemler vardır.
Sistem kısıtlamaları
iOS 10 VOIP uygulamasını çalıştıran iOS cihazın geçerli durumuna bağlı olarak bazı sistem kısıtlamaları uygulanabilir.
Örneğin, aşağıdaki gibi bir durumda gelen VOIP çağrısı Sistem tarafından kısıtlanabilir:
- Çağıran kişi kullanıcının Engellenen Çağıran Listesi'ne eklenir.
- 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 çağrısı sırasında gerektirecek ses kaynaklarını işlemeye çeşitli avantajlar sağlar. En büyük avantajlarından biri, uygulamanın ses oturumunun iOS 10'da çalıştırıldıklarında yükseltilmiş önceliklere sahip 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ı önlemektedir.
Ayrıca CallKit, performansı geliştiren ve kullanıcı tercihlerine ve cihaz durumlarına göre canlı arama sırasında VOIP seslerini belirli çıkış cihazlarına akıllı bir şekilde yönlendiren diğer ses yönlendirme ipuçlarına da erişim sağlar. Örneğin, Bluetooth bluetooth bluetooth bluetooth bluetooth cihazları, canlı bir 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 bu çağrıyı sağlaycı Ses Akışını yapılandırması gerekir. Aşağıdaki örneği göz atarak:
- Uygulama tarafından gelen bir çağrıyı yanıtlamak için Bir Çağrı Başlatma Eylemi alınmıştır.
- Bu Eylem uygulama tarafından yerine getirilmeden önce, için gerekli olan yapılandırmayı
AVAudioSessionsağlar. - Uygulama, sisteme eylemin karşılandığını bildirir.
- Çağrı bağlanmadan önce, CallKit,
AVAudioSessionuygulamanın istediği yapılandırmayla eşleşen yüksek öncelikli bir yapılandırma sağlar. Uygulamasına uygulamasının yöntemi aracılığıyla bildirim alınacaktırDidActivateAudioSessionCXProviderDelegate.
Arama dizini uzantılarıyla çalışma
CallKit ile çalışırken, çağrı dizini uzantıları , engellenen çağrı numaralarını eklemek ve belırlı bir VoIP uygulamasına özgü sayıları IOS cihazında kişi uygulamasındaki kişilere göre tanımlamak için bir yol sağlar.
Çağrı dizini uzantısı uygulama
Bir Xamarin. iOS uygulamasında bir ç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 ekle yeni Projectekle ' yi seçin.
İOSuzantılarıçağrı dizini uzantıları ' nı seçin ve İleri düğmesine tıklayın:
Uzantı için bir ad girin ve İleri düğmesine tıklayın:
gerekirse Project adı ve/veya çözüm adını ayarlayın ve oluştur düğmesine tıklayın:
Bu, CallDirectoryHandler.cs projeye aşağıdaki gibi görünen bir 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 Işleyicisindeki yöntemin, gerekli işlevselliği sağlamak için değiştirilmesi gerekir. Yukarıdaki örnek söz konusu olduğunda, VOıP uygulamasının kişiler veritabanındaki engellenen ve kullanılabilir sayıların listesini ayarlamaya çalışır. Her türlü istek herhangi bir nedenle başarısız olursa, NSError hatayı betimleyen ve sınıfın yöntemine geçiren bir oluşturma işlemi oluşturun CancelRequestCXCallDirectoryExtensionContext .
Engellenen sayıları ayarlamak için AddBlockingEntry sınıfının yöntemini kullanın CXCallDirectoryExtensionContext . Yöntemine girilen sayılar sayısal olarak artan düzende olmalıdır. Çok sayıda telefon numarası olduğunda en iyi performans ve bellek kullanımı için, yalnızca belirli bir zamanda bir sayı alt kümesini yüklemeyi düşünün ve yüklenen her bir numara kümesi için ayrılan nesneleri serbest bırakma Havuzu (ler) i kullanın.
VOıP uygulamasıyla bilinen kişi numaralarının uygulamasıyla Iletişim kurmak için, AddIdentificationEntry sınıfının yöntemini kullanın CXCallDirectoryExtensionContext ve hem numarayı hem de bir tanımlayıcı etiketi sağlayın. Yine, yönteme girilen 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, yalnızca belirli bir zamanda bir sayı alt kümesini yüklemeyi düşünün ve yüklenen her bir numara kümesi için ayrılan nesneleri serbest bırakma Havuzu (ler) i kullanın.
Özet
Bu makalede, Apple 'ın iOS 10 ' da piyasaya sürülen ve Xamarin. iOS VOıP uygulamalarında nasıl uygulanacağı yeni CallKit API 'SI ele alınmıştır. callkit 'in bir uygulamanın ios sistemiyle nasıl tümleştirileceğini, yerleşik uygulamalarla nasıl özellik eşliği sağladığını (Telefon gibi) ve bir uygulamanın, kilit ve giriş ekranları, Siri etkileşimleri ve kişiler uygulamaları aracılığıyla, bir uygulamanın ios genelinde görünürlüğünü nasıl artıracağını göstermiştir.












