<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0">

<channel>

<title>Just Developer Blog</title>
<link>http://bibobo.ru/</link>
<description></description>
<generator>E2 (v3254; Aegea)</generator>

<item>
<title>Trivia Quiz App Got Noticed</title>
<guid isPermaLink="false">35</guid>
<link>http://bibobo.ru/all/trivia-quiz-app-got-noticed/</link>
<comments>http://bibobo.ru/all/trivia-quiz-app-got-noticed/</comments>
<description>&lt;p&gt;Finally, after almost 2 years in AppStore my app Trivia Quiz Classic got noticed by one of the bloggers. Here is the article he wrote «Underrated Games To Tryout in December 2020» and Trivia Quiz is one of these apps.&lt;br /&gt;
Please take a look at the article: &lt;a href="https://gameskeys.net/underrated-games-to-tryout-in-december-2020"&gt;https://gameskeys.net/underrated-games-to-tryout-in-december-2020&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Mon, 07 Dec 2020 22:18:38 +0300</pubDate>
</item>

<item>
<title>Запуск консольных утилит из скрипта на Swift</title>
<guid isPermaLink="false">34</guid>
<link>http://bibobo.ru/all/zapusk-konsolnyh-utilit-iz-skripta-na-swift/</link>
<comments>http://bibobo.ru/all/zapusk-konsolnyh-utilit-iz-skripta-na-swift/</comments>
<description>&lt;p&gt;В процессе написания утилит на &lt;i&gt;Swift&lt;/i&gt; бывает необходимо запустить другую консольную утилиту с параметрами. Поскольку в &lt;i&gt;Swift&lt;/i&gt; доступны для использования все стандартные библиотеки &lt;i&gt;macOS&lt;/i&gt; и &lt;i&gt;iOS&lt;/i&gt; мы можем с легкостью это сделать, используя &lt;i&gt;API&lt;/i&gt; фреймворка &lt;i&gt;Foundation&lt;/i&gt;.&lt;/p&gt;
&lt;p&gt;&lt;input id="button_swift_from_terminal_run" type="button" onclick="toggle(cut_swift_from_terminal_run, button_swift_from_terminal_run)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/p&gt;
&lt;div id="cut_swift_from_terminal_run" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;Предположим на нужно запустить утилиту &lt;i&gt;plutil&lt;/i&gt;, которая работает с файлами &lt;i&gt;plist&lt;/i&gt; на проверку определенного файла на валидность. Это можно сделать при помощи следующего кода:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;//создадим экземпляр класса NSTask
let task = NSTask()

//в поле launchPath зададим путь к утилите, которую хотим запустить
task.launchPath = &amp;quot;/usr/bin/plutil&amp;quot;

//в arguments поместим массив с аргументами для plutil
task.arguments = [&amp;quot;-lint&amp;quot;, path]

//создадим экземпляр NSPipe, чтобы получить результат работы plutil и позже обработать его
let pipe = NSPipe()

//наш pipe присвоим переменной standardOutput таска, чтобы он знал с каким потоком ввода/вывода работать
task.standardOutput = pipe

//синхронно стартуем таск на выполнение
task.launch()

//чтобы получить результат работы plutil достанем хэндлер файла для чтения результата и прочтем его в константу data
let data = pipe.fileHandleForReading.readDataToEndOfFile()

//последнее, что остается сделать - создать строку из данных
//используем конструкцию с проверкой на nil, чтобы не нарваться на пустое значение и не уронить наш скрипт
if let plutilResult = NSString(data: data, encoding: NSUTF8StringEncoding)
{
	print(plutilResult)
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Tue, 19 Feb 2019 19:34:22 +0300</pubDate>
</item>

<item>
<title>Запуск Swift-скриптов из командной строки</title>
<guid isPermaLink="false">33</guid>
<link>http://bibobo.ru/all/zapusk-swift-skriptov-iz-komandnoy-stroki/</link>
<comments>http://bibobo.ru/all/zapusk-swift-skriptov-iz-komandnoy-stroki/</comments>
<description>&lt;p&gt;Для автоматизации рутинных задач очень удобно писать маленькие скрипты, которые выполняют ту или иную функцию. Для этих целей я в основном использую &lt;i&gt;Python&lt;/i&gt; и &lt;i&gt;Swift&lt;/i&gt;. Если с запуском &lt;i&gt;Python&lt;/i&gt;-скриптов все просто, то со &lt;i&gt;Swift&lt;/i&gt; есть некоторые нюансы. О них вы узнаете из этой статьи.&lt;/p&gt;
&lt;p&gt;&lt;input id="button_swift_from_terminal" type="button" onclick="toggle(cut_swift_from_terminal, button_swift_from_terminal)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/p&gt;
&lt;div id="cut_swift_from_terminal" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;Из консоли можно запустить как бинарник программы, написанной на &lt;i&gt;Swift&lt;/i&gt;, так и сам скрипт. Обычно я выбираю второй вариант, потому что с ним проще, его можно в любой момент отредактировать или доработать.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;Для того, чтобы запустить на исполнение &lt;i&gt;*.swift&lt;/i&gt; файл первым делом необходимо первой строкой в файле написать следующий код&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;#!/usr/bin/env xcrun swift&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Этот код даст указание башу на то, чем необходимо исполнять код в файле, в нашем случае это будет &lt;i&gt;xcrun&lt;/i&gt;. Далее следует код нашего скрипта.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;После того, как программа закончена, необходимо сделать файл скрипта исполняемым, для этого в консоли необходимо написать следующее:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;chmod +x имя_нашего_скрипта.swift&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Тут мы делаем скрипт исполняемым.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;Далее чтобы запустить скрипт в консоли нужно перейти в директорию, где он находится и написать следующее:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;./имя_нашего_скрипта.swift&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Вот и все, теперь можно писать различную автоматизацию на этом замечательном языке от компании &lt;i&gt;Apple&lt;/i&gt; :)&lt;/p&gt;
&lt;/div&gt;</description>
<pubDate>Tue, 19 Feb 2019 19:28:00 +0300</pubDate>
</item>

<item>
<title>Чтение параметров командной строки в Swift-скрипте</title>
<guid isPermaLink="false">32</guid>
<link>http://bibobo.ru/all/chtenie-parametrov-komandnoy-stroki-v-swift-skripte/</link>
<comments>http://bibobo.ru/all/chtenie-parametrov-komandnoy-stroki-v-swift-skripte/</comments>
<description>&lt;p&gt;При запуске программ на &lt;i&gt;Swift&lt;/i&gt; из командной строки бывает необходимо передать параметры скрипту. Это удобно, когда нужно специфицировать поведение программы. Например заставить ее просканировать все дерево каталогов на наличие файлов, либо указать на конкретный файл для обработки.&lt;/p&gt;
&lt;p&gt;&lt;input id="button_swift_params" type="button" onclick="toggle(cut_swift_params, button_swift_params)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/p&gt;
&lt;div id="cut_swift_params" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;Для этих целей в стандартной библиотеке &lt;i&gt;Swift&lt;/i&gt; существует класс &lt;i&gt;Process&lt;/i&gt;, который содержит массив строк, под названием &lt;i&gt;arguments&lt;/i&gt;. Следующий код выведет все аргументы, переданные скрипту:&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;for argument in Process.arguments
{
	print(argument);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;При этом первым аргументом будет передан путь к файлу скрипта и его имя.&lt;/p&gt;
&lt;p&gt;Посмотрим, что будет содержаться в массиве arguments если вызвать скрипт следующим способом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;./script.swift -- /simple/path/to/directory file_name_to_find&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Тогда содержимое Process.arguments будет следующим:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;Process.arguments[0] = “./script.swift”
Process.arguments[1] = “--”
Process.arguments[2] = “/simple/path/to/directory”
Process.arguments[3] = “file_name_to_find”&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Tue, 19 Feb 2019 19:12:00 +0300</pubDate>
</item>

<item>
<title>Получение информации о состоянии батареи в MacOS (выполнением консольной команды)</title>
<guid isPermaLink="true">http://bibobo.ru/all/poluchenie-informacii-o-sostoyanii-batarei-v-macos-vypolneniem-k/</guid>
<link>http://bibobo.ru/all/poluchenie-informacii-o-sostoyanii-batarei-v-macos-vypolneniem-k/</link>
<comments>http://bibobo.ru/all/poluchenie-informacii-o-sostoyanii-batarei-v-macos-vypolneniem-k/</comments>
<description>&lt;p&gt;После обновления &lt;i&gt;MacOS&lt;/i&gt; до &lt;i&gt;Sierra&lt;/i&gt; я столкнулся с проблемой — в статус баре исчезла информация об оставшемся времени работы от батареи. Проблема на самом деле решается очень просто — можно установить стороннее приложение &lt;a href="https://github.com/codler/Battery-Time-Remaining"&gt;Battery-Time-Remaining&lt;/a&gt;, которое является настоящим швейцарским ножом в области работы с батареей макбука. Но мне захотелось разобраться как можно получить информацию об оставшемся времени работы от батареи в &lt;i&gt;MacOS&lt;/i&gt; и написать свое приложение для статус бара.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;Существует два способа получения состояния батареи в &lt;i&gt;MacOS&lt;/i&gt;:&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;(правильный) при помощи набора функций &lt;a href="https://developer.apple.com/reference/iokit"&gt;IOPowerFunctions&lt;/a&gt; фреймворка &lt;i&gt;IOKit&lt;/i&gt;;&lt;/li&gt;
&lt;li&gt;(быстрый) выполнением консольной команды “&lt;b&gt;pmset -g batt&lt;/b&gt;” и парсинга вывода.&lt;br /&gt;
&lt;input id="button_macos_pmset_battery_remaining" type="button" onclick="toggle(cut_macos_pmset_battery_remaining, button_macos_pmset_battery_remaining)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div id="cut_macos_pmset_battery_remaining" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Воспользуемся вторым способом (приложение не нацелено на продакшн и его нужно реализовать быстро) и напишем приложение, которое будет выводить оставшееся время работы ноутбука от батареи в статус баре.&lt;/p&gt;
&lt;p&gt;Принцип работы будет следующим:&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;используя &lt;i&gt;IOKit&lt;/i&gt; подпишемся на нотификации системы об изменении состояния батареи для обновления данных в статус баре (можно конечно сделать при помощи таймера, но это лишняя трата ресурсов системы)&lt;/li&gt;
&lt;li&gt;при изменении состояния батареи используя &lt;i&gt;NSTask&lt;/i&gt; выполняем консольную команду и захватываем ее вывод&lt;/li&gt;
&lt;li&gt;парсим вывод и пересоздаем меню в статус баре&lt;br /&gt;
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Чтобы все успешно заработало необходимо добавить &lt;i&gt;IOKit.framework&lt;/i&gt; и зареференсить хедер &lt;i&gt;IOKit/ps/IOPowerSources.h&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;Так же, нужно не забыть снять галочку &lt;i&gt;Visible At Launch&lt;/i&gt; с &lt;i&gt;NSWindow&lt;/i&gt; в &lt;i&gt;Xib&lt;/i&gt; интерфейса приложения (это запретит окну автоматически появляться на экране, нам нужен только статус бар)&lt;/p&gt;
&lt;p&gt;Чтобы скрыть иконку приложения из дока &lt;i&gt;MacOS&lt;/i&gt; нужно в &lt;i&gt;info.plist&lt;/i&gt; установить флаг &lt;i&gt;LSUIElement&lt;/i&gt; в значение &lt;i&gt;YES&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Capture Power Source updates and make sure our callback is called
    CFRunLoopSourceRef loop = IOPSNotificationCreateRunLoopSource(PowerSourceChanged, (__bridge void *)self);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, kCFRunLoopDefaultMode);
    CFRelease(loop);
    
    self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
    self.statusItem.title = @&amp;quot;Checking...&amp;quot;;
    self.statusItem.highlightMode = YES;
    
    //request battery status for the first time
    [self requestBatteryStatus];
}

// IOPS notification callback on power source change
static void PowerSourceChanged(void * context)
{
    //requesting battery status
    AppDelegate *self = (__bridge AppDelegate *)context;
    [self requestBatteryStatus];
}

-(void)requestBatteryStatus
{
    //create task &amp;amp; launch 'pmset' to get battery status string
    NSPipe *pipe = [NSPipe pipe];
    NSFileHandle *file = pipe.fileHandleForReading;
    
    NSTask *task = [[NSTask alloc] init];
    task.launchPath = @&amp;quot;/usr/bin/pmset&amp;quot;;
    task.arguments = @[@&amp;quot;-g&amp;quot;, @&amp;quot;batt&amp;quot;];
    task.standardOutput = pipe;
    
    [task launch];
    
    NSData *data = [file readDataToEndOfFile];
    [file closeFile];
    
    //parse string to get particular items
    NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSString *statusData = [result componentsSeparatedByString:@&amp;quot;	&amp;quot;][1];
    NSArray *status = [statusData componentsSeparatedByString:@&amp;quot;;&amp;quot;];
    
    //battery charge percent
    NSString *batteryPercent = [status[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    //charging status
    NSString *batteryChanging = [status[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    
    //ETA
    NSString *batteryEstimate = [status[2] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    //remaining hours
    NSString *batteryRemaining = batteryEstimate;
    NSRange range = [batteryEstimate rangeOfString:@&amp;quot;present&amp;quot;];
    if(range.length &amp;gt; 0){
        batteryEstimate = [[[batteryEstimate substringToIndex:range.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@&amp;quot;()&amp;quot;]];
        
        batteryRemaining = [batteryEstimate componentsSeparatedByString:@&amp;quot; &amp;quot;][0];
    }
    
    //recrete status bar menu
    NSMenu *menu = [[NSMenu alloc] init];
    [menu addItemWithTitle:batteryPercent action:nil keyEquivalent:@&amp;quot;&amp;quot;];
    [menu addItemWithTitle:batteryEstimate action:nil keyEquivalent:@&amp;quot;&amp;quot;];
    [menu addItemWithTitle:batteryChanging action:nil keyEquivalent:@&amp;quot;&amp;quot;];
    
    [menu addItem:[NSMenuItem separatorItem]];
    
    [menu addItemWithTitle:@&amp;quot;Exit&amp;quot; action:@selector(exitAction:) keyEquivalent:@&amp;quot;&amp;quot;];
    
    self.statusItem.menu = menu;
    
    self.statusItem.title = [batteryRemaining stringByAppendingString:@&amp;quot; ETA&amp;quot;];
    
    NSLog(@&amp;quot;Request success!&amp;quot;);
}

-(void)exitAction:(id)sender
{
    exit(0);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Mon, 02 Jan 2017 12:44:55 +0300</pubDate>
</item>

<item>
<title>Работа с AVAudioPlayer и MPNowPlayingInfo (iOS)</title>
<guid isPermaLink="true">http://bibobo.ru/all/rabota-s-avaudioplayer-i-mpnowplayinginfo-ios/</guid>
<link>http://bibobo.ru/all/rabota-s-avaudioplayer-i-mpnowplayinginfo-ios/</link>
<comments>http://bibobo.ru/all/rabota-s-avaudioplayer-i-mpnowplayinginfo-ios/</comments>
<description>&lt;p&gt;В ходе разработки приложений под iOS часто возникает необходимость проиграть аудио-файл, например композицию или эффект. Наиболее простым способом для этого в iOS является использование класса AVAudioPlayer. Он является частью фреймворка AVFoundation, так что нам потребуется поставить на него using.&lt;/p&gt;
&lt;p&gt;Ниже представлен код хэлпера, упрощающего работу с этим классом.&lt;/p&gt;
&lt;p&gt;&lt;input id="button_ios_avaudioplayer" type="button" onclick="toggle(cut_ios_avaudioplayer, button_ios_avaudioplayer)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/p&gt;
&lt;div id="cut_ios_avaudioplayer" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Хэлпер, упрощающий работу с плейером (в системе всегда висит один статический плейер)
/// &amp;lt;/summary&amp;gt;
public static class PlayerHelper
{
    /// &amp;lt;summary&amp;gt;
    /// Состояние плейера
    /// &amp;lt;/summary&amp;gt;
    public enum PlayerState
    {
        Uncknown,
        Playing,
        Stopped,
        Paused
    }

    /// &amp;lt;summary&amp;gt;
    /// Состояние плейера
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;value&amp;gt;The state.&amp;lt;/value&amp;gt;
    public static PlayerState State { get; private set; }

    private static AVAudioPlayer _player;
    public static AVAudioPlayer Player { get{ return _player; } }

    /// &amp;lt;summary&amp;gt;
    /// Включена или нет функция зацикливания
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;value&amp;gt;&amp;lt;c&amp;gt;true&amp;lt;/c&amp;gt; if is infinite playing; otherwise, &amp;lt;c&amp;gt;false&amp;lt;/c&amp;gt;.&amp;lt;/value&amp;gt;
    public static bool IsInfinitePlaying
    {
        get{ return _player != null ? _player.NumberOfLoops == int.MaxValue : false; }
        set
        {
            if (_player == null)
            {
                return;
            }

            _player.NumberOfLoops = value ? int.MaxValue : 0;
        }
    }

    static PlayerHelper()
    {
        State = PlayerState.Uncknown;
    }

    /// &amp;lt;summary&amp;gt;
    /// Проиграть URL с диска (если он уже проигрывается, воспроизведение продолжится с места остановки)
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;The URL.&amp;lt;/returns&amp;gt;
    /// &amp;lt;param name=&amp;quot;url&amp;quot;&amp;gt;URL.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;finishedPlayingAction&amp;quot;&amp;gt;Finished playing action.&amp;lt;/param&amp;gt;
    public static AVAudioPlayer PlayUrl(NSUrl url, Action finishedPlayingAction = null)
    {
        //в тех случаях, когда плейер уже проигрывает переданный url - не нужно останавливать запись или начинать сначала
        if (IsPlayerLoadedThisFile(url))
        {
            ResumePlaying();
            return _player;
        }

        StopPlaying();

        _player = AVAudioPlayer.FromUrl(url);
        _player.CurrentTime = 0;
        _player.Volume = 1f;

        if (finishedPlayingAction != null)
        {
            _player.FinishedPlaying += (sender, e) =&amp;gt;
            {
                finishedPlayingAction.Invoke();
            };
        }

        _player.PrepareToPlay();

        //разрешаем проигрывание в бекграунде
        AVAudioSession.SharedInstance().SetCategory(AVAudioSessionCategory.Playback);
        AVAudioSession.SharedInstance().SetActive(true);
        UIApplication.SharedApplication.BeginReceivingRemoteControlEvents();

        _player.Play();

        State = PlayerState.Playing;

        return _player;
    }

    /// &amp;lt;summary&amp;gt;
    /// Проиграть
    /// &amp;lt;/summary&amp;gt;
    public static void Play()
    {
        if (_player == null)
        {
            return;
        }

        _player.Play();
        State = PlayerState.Playing;
    }

    /// &amp;lt;summary&amp;gt;
    /// Остановить
    /// &amp;lt;/summary&amp;gt;
    public static void StopPlaying()
    {
        if (_player == null)
        {
            return;
        }

        _player.Stop();
        State = PlayerState.Stopped;
    }

    /// &amp;lt;summary&amp;gt;
    /// Приостановить
    /// &amp;lt;/summary&amp;gt;
    public static void PausePlaying()
    {
        if (_player == null)
        {
            return;
        }

        _player.Pause();
        State = PlayerState.Paused;
    }

    /// &amp;lt;summary&amp;gt;
    /// Продолжить
    /// &amp;lt;/summary&amp;gt;
    public static void ResumePlaying()
    {
        if (_player == null)
        {
            return;
        }

        _player.Play();
        State = PlayerState.Playing;
    }

    /// &amp;lt;summary&amp;gt;
    /// Возвратит true, если плейер загрузил файл по этому URL с диска
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;c&amp;gt;true&amp;lt;/c&amp;gt; if is player loaded this file the specified url; otherwise, &amp;lt;c&amp;gt;false&amp;lt;/c&amp;gt;.&amp;lt;/returns&amp;gt;
    /// &amp;lt;param name=&amp;quot;url&amp;quot;&amp;gt;URL.&amp;lt;/param&amp;gt;
    public static bool IsPlayerLoadedThisFile(NSUrl url)
    {
        if (_player == null)
        {
            return false;
        }

        return _player.Url.LastPathComponent == url.LastPathComponent;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;Так же при проигрывании композиции (если в info.plist прописано разрешение на проигрывание аудио в бекграунде) на главном экране отображается информация о композиции, а так же кнопки управления воспроизведением. Чтобы информация отображалась корректно необходимо при старте проигрывания трека написать следующий код:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;MPNowPlayingInfo info = new MPNowPlayingInfo();
info.Title = &amp;quot;Имя композиции&amp;quot;;
info.Artist = &amp;quot;наименование артиста&amp;quot;;

info.PlaybackRate = 1f;
info.PlaybackDuration = PlayerHelper.Player.Duration;
info.ElapsedPlaybackTime = PlayerHelper.Player.CurrentTime;

MPNowPlayingInfoCenter.DefaultCenter.NowPlaying = info;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Mon, 18 Jan 2016 23:11:20 +0300</pubDate>
</item>

<item>
<title>Использование QLPreviewController для просмотра файлов (iOS)</title>
<guid isPermaLink="true">http://bibobo.ru/all/ispolzovanie-qlpreviewcontroller-dlya-prosmotra-faylov-ios/</guid>
<link>http://bibobo.ru/all/ispolzovanie-qlpreviewcontroller-dlya-prosmotra-faylov-ios/</link>
<comments>http://bibobo.ru/all/ispolzovanie-qlpreviewcontroller-dlya-prosmotra-faylov-ios/</comments>
<description>&lt;p&gt;Для реализации просмотра файлов различных форматов (изображения, word, excel, txt, медиа и т. п.) в iOS существует очень удобный класс QLPreviewController. Для его удобного использования необходимо сделать следующее:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Создать класс-наследник QLPreviewController, который инкапсулирует все детали реализации&lt;/li&gt;
&lt;li&gt;Создать класс QLPreviewControllerDataSource, который будет являться источником данных для контроллера&lt;/li&gt;
&lt;li&gt;Создать классы-наследники QLPreviewItem, которые будут хранить информацию о просматриваемом элементе&lt;br /&gt;
&lt;input id="button_ios_qlpreview" type="button" onclick="toggle(cut_ios_qlpreview, button_ios_qlpreview)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div id="cut_ios_qlpreview" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Без лишних слов сразу взглянем на код контроллера&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Контроллер, который позволяет отобразить документы (изображения, ворд, экзель, пдф и т.п.) в виде превью
/// поддерживает несколько документов для отображения
/// &amp;lt;/summary&amp;gt;
public class QuickLookPreviewController : QLPreviewController
{
    /// &amp;lt;summary&amp;gt;
    /// Отобразить список NSUrl'ов
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;c&amp;gt;true&amp;lt;/c&amp;gt;, if urls was previewed, &amp;lt;c&amp;gt;false&amp;lt;/c&amp;gt; otherwise.&amp;lt;/returns&amp;gt;
    /// &amp;lt;param name=&amp;quot;urls&amp;quot;&amp;gt;Urls.&amp;lt;/param&amp;gt;
    public bool PreviewUrls(params NSUrl[] urls)
    {
        List&amp;lt;QLPreviewItem&amp;gt; items = new List&amp;lt;QLPreviewItem&amp;gt;();
        foreach (NSUrl url in urls)
        {
            QLPreviewItemNsUrl item = new QLPreviewItemNsUrl(url);
            if (QLPreviewController.CanPreviewItem(item))
            {
                items.Add(item);
            }
        }

        return PreviewItems(items.ToArray());
    }

    /// &amp;lt;summary&amp;gt;
    /// Отобразить список QLPreviewItem
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;c&amp;gt;true&amp;lt;/c&amp;gt;, if items was previewed, &amp;lt;c&amp;gt;false&amp;lt;/c&amp;gt; otherwise.&amp;lt;/returns&amp;gt;
    /// &amp;lt;param name=&amp;quot;items&amp;quot;&amp;gt;Items.&amp;lt;/param&amp;gt;
    public bool PreviewItems(params QLPreviewItem[] items)
    {
        if (items.Length &amp;gt; 0)
        {
            DataSource = new PreviewControllerDataSource(items);
            return true;
        }

        return false;
    }

    /// &amp;lt;summary&amp;gt;
    /// QL Preview источник данных
    /// &amp;lt;/summary&amp;gt;
    private class PreviewControllerDataSource : QLPreviewControllerDataSource
    {
        private QLPreviewItem[] _items = new QLPreviewItem[0];

        public PreviewControllerDataSource(params QLPreviewItem[] items)
        {
            _items = items;
        }

        public override nint PreviewItemCount(QLPreviewController controller)
        {
            return _items.Length;
        }

        public override IQLPreviewItem GetPreviewItem(QLPreviewController controller, nint index)
        {
            return _items[index];
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// QL Preview Item, принимающий NSUrl
    /// &amp;lt;/summary&amp;gt;
    public class QLPreviewItemNsUrl : QLPreviewItem
    {
        private NSUrl _documentUrl;

        public QLPreviewItemNsUrl(NSUrl documentUrl)
        {
            _documentUrl = documentUrl;
        }

        public override string ItemTitle
        {
            get
            {
                return _documentUrl.LastPathComponent;
            }
        }
        public override NSUrl ItemUrl
        {
            get
            {
                return _documentUrl;
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// QL Preview Item, принимающий имя и путь к файлу из файловой системы
    /// &amp;lt;/summary&amp;gt;
    public class QLPreviewItemFileSystem : QLPreviewItem
    {
        string _fileName, _filePath;

        public QLPreviewItemFileSystem(string fileName, string filePath)
        {
            _fileName = fileName;
            _filePath = filePath;
        }

        public override string ItemTitle
        {
            get
            {
                return _fileName;
            }
        }
        public override NSUrl ItemUrl
        {
            get
            {
                return NSUrl.FromFilename(_filePath);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// QL Preview Item, принимающий имя ипуть к файлу из бандла
    /// &amp;lt;/summary&amp;gt;
    public class QLPreviewItemBundle : QLPreviewItem
    {
        string _fileName, _filePath;
        public QLPreviewItemBundle(string fileName, string filePath)
        {
            _fileName = fileName;
            _filePath = filePath;
        }

        public override string ItemTitle
        {
            get
            {
                return _fileName;
            }
        }
        public override NSUrl ItemUrl
        {
            get
            {
                var documents = NSBundle.MainBundle.BundlePath;
                var lib = Path.Combine(documents, _filePath);
                var url = NSUrl.FromFilename(lib);
                return url;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;Использовать данный код можно следующим образом:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;QuickLookPreviewController previewController = new QuickLookPreviewController();
if (previewController.PreviewUrls(new NSUrl[]{ fileUrl }))
{
    this.NavigationController.PushViewController(previewController, true);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Wed, 13 Jan 2016 22:10:14 +0300</pubDate>
</item>

<item>
<title>Создание и удаление записей в календаре (iOS)</title>
<guid isPermaLink="true">http://bibobo.ru/all/sozdanie-i-udalenie-zapisey-v-kalendare-ios/</guid>
<link>http://bibobo.ru/all/sozdanie-i-udalenie-zapisey-v-kalendare-ios/</link>
<comments>http://bibobo.ru/all/sozdanie-i-udalenie-zapisey-v-kalendare-ios/</comments>
<description>&lt;p&gt;Многие приложения используют очень полезную фишку ОС от Apple как создание кастомных записей и календарей. Сегодня мы рассмотрим пример реализации класса-хэлпера, позволяющего создавать и удалять записи в кастомном календаре.&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;Вся работа с календарем строится через фреймворк EventKit, при этом последовательность действий для создания записи примерно следующая:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Создать экземпляра класса EKEventStore&lt;/li&gt;
&lt;li&gt;Запросить через него разрешение на работу с календарем&lt;/li&gt;
&lt;li&gt;В completionHandler реализовать код, который будет создавать запись&lt;/li&gt;
&lt;li&gt;При создании записи необходимо сначала найти наш кастомный календарь EKCalendar (или создать его, если его еще нет)&lt;/li&gt;
&lt;li&gt;Создать экземпляр класса EKEvent (это и будет наша запись) и установить для него заголовок, сопроводительный текст, время начала и окончания&lt;/li&gt;
&lt;li&gt;Создать и добавить ремайндер (экземпляр класса EKAlarm), который описывает когда должно сработать напоминание о записи&lt;br /&gt;
&lt;input id="button_ios_manage_calendar_events" type="button" onclick="toggle(cut_ios_manage_calendar_events, button_ios_manage_calendar_events)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div id="cut_ios_manage_calendar_events" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Более подробно можно понять, посмотрев код:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;public class EventKitCalendarHelper
{
    /// &amp;lt;summary&amp;gt;
    /// Имя кастомного календаря
    /// &amp;lt;/summary&amp;gt;
    public static string CalendarTitle = &amp;quot;CalendarTitle&amp;quot;;

    /// &amp;lt;summary&amp;gt;
    /// Варианты срабатывания напоминания для записи в календаре
    /// &amp;lt;/summary&amp;gt;
    public enum AlarmTime
    {
        None = 0,
        FifteenMinutesBefore = -15*60,
        HalfAnHourBefore = -30*60,
        AnHourBefore = -60*60,
        TwoHoursBefore = -2*60*60,
        ThreeHoursBefore = -3*60*60
    }

    /// &amp;lt;summary&amp;gt;
    /// Добавляет запись в календаре
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&amp;quot;eventTitle&amp;quot;&amp;gt;Event title.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;dateStart&amp;quot;&amp;gt;Date start.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;timeLength&amp;quot;&amp;gt;Time length.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;time&amp;quot;&amp;gt;Time.&amp;lt;/param&amp;gt;
    public static void AddCalendarEvent(string eventTitle, DateTime dateStart, TimeSpan timeLength, string eventNotes = &amp;quot;&amp;quot;, AlarmTime time = AlarmTime.None)
    {
        if (string.IsNullOrEmpty(eventTitle))
        {
            return;
        }

        EKEventStore store = new EKEventStore();
        store.RequestAccess(EKEntityType.Event, (granted, err) =&amp;gt;
            {
                if (!granted)
                {
                    return;
                }

                EKEvent entry = EKEvent.FromStore(store);
                entry.Title = eventTitle;
                entry.Notes = eventNotes;
                entry.StartDate = dateStart.DateTimeToNSDate();
                entry.EndDate = dateStart.Add(timeLength).DateTimeToNSDate();

                //ищем наш кастомный календарь, если мы его не находим - создаем новый
                EKCalendar calendar = FindOrCreateCalendar(store, true);
                if(calendar == null)
                {
                    return;
                }

                entry.Calendar = calendar;

                //создаем ремайндер, если он требуется
                if (time != AlarmTime.None)
                {
                    entry.AddAlarm(EKAlarm.FromTimeInterval((int)time));
                }

                err = null;
                store.SaveEvent(entry, EKSpan.ThisEvent, out err);
            });
    }

    /// &amp;lt;summary&amp;gt;
    /// Находит или создает кастомный календарь
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;The or create calendar.&amp;lt;/returns&amp;gt;
    /// &amp;lt;param name=&amp;quot;store&amp;quot;&amp;gt;Store.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;createIfDoesntExist&amp;quot;&amp;gt;If set to &amp;lt;c&amp;gt;true&amp;lt;/c&amp;gt; create if doesnt exist.&amp;lt;/param&amp;gt;
    private static EKCalendar FindOrCreateCalendar(EKEventStore store, bool createIfDoesntExist = false)
    {
        NSError err = null;

        EKCalendar calendar = store.Calendars.Where(x =&amp;gt; x.Title == CalendarTitle).FirstOrDefault();
        if (calendar == null &amp;amp;&amp;amp; createIfDoesntExist)
        {
            err = null;
            calendar = EKCalendar.Create(EKEntityType.Event, store);
            calendar.Title = CalendarTitle;

            EKSource localSource = store.Sources.Where(x =&amp;gt; x.SourceType == EKSourceType.Local).FirstOrDefault();
            calendar.Source = localSource;

            store.SaveCalendar(calendar, true, out err);
        }

        return calendar;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;С удалением событий дела обстоят интереснее. При создании события у него есть строковое поле EventIdentifier, его можно где-то сохранить и использовать позже для удаления записи. Либо можно найти все записи при помощи специальным образом сформированного предиката и удалить их. Рассмотрим второй вариант.&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Удаляет записи в календаре, которые соответствуют выбранному интервалу времени
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&amp;quot;dateStart&amp;quot;&amp;gt;Date start.&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&amp;quot;timeLength&amp;quot;&amp;gt;Time length.&amp;lt;/param&amp;gt;
public static void RemoveCalendarEvent(DateTime dateStart, TimeSpan timeLength, string title = &amp;quot;&amp;quot;)
{
    EKEventStore store = new EKEventStore();
    store.RequestAccess(EKEntityType.Event, (granted, err) =&amp;gt;
        {
            if (!granted)
            {
                return;
            }

            //ищем наш кастомный календарь
            EKCalendar calendar = FindOrCreateCalendar(store);
            if (calendar == null)
            {
                return;
            }

            //создаем предикат для поиска эвентов в нашем календаре
            NSPredicate predicate = store.PredicateForEvents(dateStart.DateTimeToNSDate(), dateStart.Add(timeLength).DateTimeToNSDate(), new EKCalendar[]{ calendar });
            EKEvent[] entries = store.EventsMatching(predicate);

            if (entries == null || entries.Length == 0)
            {
                return;
            }

            //если нужно - профильтруем элементы календаря по заголовку
            if (!string.IsNullOrEmpty(title))
            {
                entries = entries.Where(x =&amp;gt; x.Title == title).ToArray();
            }

            if (entries == null || entries.Length == 0)
            {
                return;
            }

            //проходимся по всем найденным эвентам и удаляем их
            foreach (EKEvent entry in entries)
            {
                err = null;
                store.RemoveEvent(entry, EKSpan.ThisEvent, true, out err);
            }
        });
}&lt;/code&gt;&lt;/pre&gt;</description>
<pubDate>Wed, 06 Jan 2016 18:38:03 +0300</pubDate>
</item>

<item>
<title>Использование NSUrlSession для загрузки файлов (iOS)</title>
<guid isPermaLink="true">http://bibobo.ru/all/ispolzovanie-nsurlsession-dlya-zagruzki-faylov-ios/</guid>
<link>http://bibobo.ru/all/ispolzovanie-nsurlsession-dlya-zagruzki-faylov-ios/</link>
<comments>http://bibobo.ru/all/ispolzovanie-nsurlsession-dlya-zagruzki-faylov-ios/</comments>
<description>&lt;p&gt;При разработке мобильного приложения очень часто возникает необходимость организовать загрузку файла по сети. При этом если разработка ведется на Xamarin высоко желание использовать стандартный WebClient для этих целей. Все бы ничего, но при использовании WebClient возникает несколько сложностей, например:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Не высокая производительность&lt;/li&gt;
&lt;li&gt;Прерывание загрузки при блокировке устройства или уходе приложения в бекграунд&lt;br /&gt;
&lt;/br&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Для решения вопроса с загрузкой файлов наиболее правильным способом будет кардинально переписать загрузку на использование NSUrlSession.&lt;/p&gt;
&lt;p&gt;&lt;input id="button_ios_download_file_in_background" type="button" onclick="toggle(cut_ios_download_file_in_background, button_ios_download_file_in_background)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/p&gt;
&lt;div id="cut_ios_download_file_in_background" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;NSUrlSession — это системный механизм iOS, позволяющий организовать длительные процессы загрузки и заливки данных по сети, при этом система сама правильно обработает обрывы связи, докачает файл, если это возможно и уведомит приложение о результате.&lt;/p&gt;
&lt;p&gt;Чтобы использовать NSUrlSession нам потребуется сделать следующее:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Создать класс-хэлпер, который будет инкапсулировать всю логику работы с NSUrlSession&lt;/li&gt;
&lt;li&gt;Создать класс-делегат наследник NSUrlSessionDownloadDelegate, который будет уведомлять внешний код о прогрессе загрузки файла, а так же об окончании загрузки&lt;/li&gt;
&lt;li&gt;Создать экземпляр класса NSUrlSession и присвоить ему делегат&lt;/li&gt;
&lt;li&gt;Для каждого URL файла создать NSUrlRequest и NSUrlSessionDownloadTask, и запустить скачивание&lt;/li&gt;
&lt;li&gt;Так же необходимо позаботиться о том, чтобы не потерять информацию о том, какому Url соответствует какой NSUrlSessionDownloadTask, чтобы потом корректно сообщать внешнему коду о ходе скачивания&lt;br /&gt;
&lt;/br&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Далее код готового решения с комментариями о деталях реализации:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Класс, который реализует всю работу с механизмом NSUrlSession для реализации загрузки файлов в фоновом режиме
/// &amp;lt;/summary&amp;gt;
public class DownloadSession
{
    /// &amp;lt;summary&amp;gt;
    /// Создаем экземпляр конфигурации для сессии (с таким ID сессия может быть только одна в приложении)
    /// &amp;lt;/summary&amp;gt;
    private static NSUrlSessionConfiguration _downloadConfiguration = NSUrlSessionConfiguration.BackgroundSessionConfiguration(&amp;quot;com.appid.background.download&amp;quot;);

    /// &amp;lt;summary&amp;gt;
    /// Класс-делегат, инкапсулирует всю логику по уведомлению внешнего кода о состоянии загрузки, а так же занимается менеджментом хранения ссылок на скачиваемые файлы
    /// &amp;lt;/summary&amp;gt;
    public sealed class ChatBackgroundSessionDelegate : NSUrlSessionDownloadDelegate
    {
        /// &amp;lt;summary&amp;gt;
        /// Словарь, который хранит соответствие - таск загрузки &amp;lt;--&amp;gt; url файла
        /// &amp;lt;/summary&amp;gt;
        public Dictionary&amp;lt;NSUrlSessionDownloadTask, string&amp;gt; QueueHolder { get; private set; }

        public delegate void DownloadingCompleteDelegate(string url, NSUrl tempFileLocation);
        public delegate void DownloadProgressDelegate(string url, int progressPercent);

        /// &amp;lt;summary&amp;gt;
        /// Экшн, который будет вызываться при окончании загрузки файла (в параметрах будут url файла и NSUrl на временный скачанный файл)
        /// &amp;lt;/summary&amp;gt;
        public DownloadingCompleteDelegate CompleteAction { get{ return _completeAction; } set{ _completeAction = value; } }
        private DownloadingCompleteDelegate _completeAction;

        /// &amp;lt;summary&amp;gt;
        /// Экшн, который будет вызываться при изменении прогресса скачивания файла (в параметрах будет url файла и процент скачивания)
        /// &amp;lt;/summary&amp;gt;
        public DownloadProgressDelegate ProgressDelegate { get{ return _progressDelegate; } set{ _progressDelegate = value; } }
        private DownloadProgressDelegate _progressDelegate;

        /// &amp;lt;summary&amp;gt;
        /// Конструктор, который создает словарь-очередь и присваивает все необходимые экшны
        /// &amp;lt;/summary&amp;gt;
        public ChatBackgroundSessionDelegate(DownloadingCompleteDelegate downloadCompleteAction, DownloadProgressDelegate progressDelegate = null)
        {
            _completeAction = downloadCompleteAction;
            _progressDelegate = progressDelegate;

            QueueHolder = new Dictionary&amp;lt;NSUrlSessionDownloadTask, string&amp;gt;();
        }

        /// &amp;lt;summary&amp;gt;
        /// Метод, вызывается системой при изменении прогресса скачивания файлов
        /// &amp;lt;/summary&amp;gt;
        public override void DidWriteData (NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)
        {
            if (_progressDelegate == null || !QueueHolder.ContainsKey(downloadTask))
            {
                return;
            }

            //если в очереди найден такой файл и экшн был передан извне - то вызываем экшн в основном потоке и вычисляем процент скачивания файла
            InvokeOnMainThread(() =&amp;gt;
                {
                    _progressDelegate.Invoke(QueueHolder[downloadTask], Convert.ToInt32(totalBytesWritten * 100 / totalBytesExpectedToWrite));
                });
        }

        /// &amp;lt;summary&amp;gt;
        /// Метод, который вызывается при завершении загрузки файла
        /// &amp;lt;/summary&amp;gt;
        public override void DidFinishDownloading(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, NSUrl location)
        {
            if (_completeAction == null || !QueueHolder.ContainsKey(downloadTask))
            {
                return;
            }

            InvokeOnMainThread(() =&amp;gt;
                {
                    _completeAction.Invoke(QueueHolder[downloadTask], location);
                });
        }
    }

    private ChatBackgroundSessionDelegate _delegate;
    private NSUrlSession _downloadSession;

    /// &amp;lt;summary&amp;gt;
    /// Конструктор класса-холдера
    /// &amp;lt;/summary&amp;gt;
    public DownloadSession(ChatBackgroundSessionDelegate.DownloadingCompleteDelegate downloadCompleteAction,
        ChatBackgroundSessionDelegate.DownloadProgressDelegate progressDelegate = null)
    {
        //создаем делегат
        _delegate = new ChatBackgroundSessionDelegate(downloadCompleteAction, progressDelegate);

        //создаем сессию (при этом если сессия с такой конфигурацией уже есть в системе - то новая сессия создана не будет, а в переменную попадет старая сессия)
        _downloadSession = NSUrlSession.FromConfiguration(
            _downloadConfiguration,
            _delegate,
            new NSOperationQueue());
    }

    /// &amp;lt;summary&amp;gt;
    /// Стартует процесс загрузки
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&amp;quot;url&amp;quot;&amp;gt;URL.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;file&amp;quot;&amp;gt;File.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&amp;quot;message&amp;quot;&amp;gt;Message.&amp;lt;/param&amp;gt;
    public void StartDownload(string url)
    {
        if (string.IsNullOrEmpty(url))
        {
            return;
        }

        NSUrl nsurl = NSUrl.FromString(new NSString(url).CreateStringByAddingPercentEscapes(NSStringEncoding.UTF8));
        NSUrlRequest request = NSUrlRequest.FromUrl(nsurl);
        NSUrlSessionDownloadTask task = _downloadSession.CreateDownloadTask(request);

        //нужно именно попробовать создать сессию и только после этого взяь ее делегат, кастануть его к нужному типу и забиндить реквесты
        //делаем так, потому что сессия глобально в системе создается только одна и при попытке повторного ее создания - новый делегат просто не присваивается :)
        //экшны копируются из нового делегата в старый, потому что мы никак физически не можем заменить делегат (он только для чтения)
        if (_delegate != (ChatBackgroundSessionDelegate)_downloadSession.Delegate)
        {
            ChatBackgroundSessionDelegate newDelegate = (ChatBackgroundSessionDelegate)_downloadSession.Delegate;
            newDelegate.CompleteAction = _delegate.CompleteAction;
            newDelegate.ProgressDelegate = _delegate.ProgressDelegate;
        }
        _delegate = (ChatBackgroundSessionDelegate)_downloadSession.Delegate;

        //добавляем наш Url и таск в очередь
        _delegate.QueueHolder.Add(task, url);

        //стартуем загрузку
        task.Resume();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;Использовать все это хозяйство можно следующим образом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;//создаем сессию загрузки данных
DownloadSession session = new DownloadSession((url, tempFileLocation) =&amp;gt;
    {
        //если завершена загрузка файла - сохраняем его на диск
        if (item.url == url)
        {
            /////////////////
        }
    }, (url, percent) =&amp;gt;
    {
        if (item.url == url)
        {
            //отображаем прогресс загрузки
        }
    });

//стартуем загрузку
session.StartDownload(item.url);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Sun, 03 Jan 2016 10:53:01 +0300</pubDate>
</item>

<item>
<title>Реализация уведомлений-индикаторов для загружаемых на сервер файлов (Android)</title>
<guid isPermaLink="true">http://bibobo.ru/all/realizaciya-uvedomleniy-indikatorov-dlya-zagruzhaemyh-na-server/</guid>
<link>http://bibobo.ru/all/realizaciya-uvedomleniy-indikatorov-dlya-zagruzhaemyh-na-server/</link>
<comments>http://bibobo.ru/all/realizaciya-uvedomleniy-indikatorov-dlya-zagruzhaemyh-na-server/</comments>
<description>&lt;p&gt;При необходимости загрузить файла на сервер из приложения встает вопрос об индикации процесса загрузки. Если дизайн позволяет то можно отобразить диалог прогресс бара (при этом заблокировать интерфейс пользователя). Но на мой взгляд более интересным вариантом индикации являются уведомления в статус баре системы, которые нельзя смахнуть.&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;При реализации такого подхода может возникнуть несколько проблем:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Как создать уведомление для каждого загружаемого файла (если например существует возможность единовременно загружать несколько файлов)&lt;/li&gt;
&lt;li&gt;Как программно отменить уведомление после завершения процесса загрузки на сервер&lt;br /&gt;
&lt;input id="button_android_download_file_notification" type="button" onclick="toggle(cut_android_download_file_notification, button_android_download_file_notification)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div id="cut_android_download_file_notification" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Для решения этих проблем необходимо знать как система индексирует уведомления, создаваемые приложением. Каждое приложение может создать сколь угодно много уведомлений, но чтобы иметь возможность обновить определенное уведомление, либо отменить его — необходимо присвоить каждому из уведомлений свой уникальный ID. Ниже код реализации создания уведомления при старте загрузки файла на сервер и его отмены, по окончании загрузки:&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;//Кеш, хранящий список id активных на данный момент уведомлений
private List&amp;lt;int&amp;gt; _notificationCache = new List&amp;lt;int&amp;gt;();

private void UploadFile()
{
    //получим рандомный ID для уведомления, при этом он не должен пересекаться с уже существующими (для этого используем кеш)
    Random r = new Random(System.Environment.TickCount);
    int notificationId = 0;
    while (notificationId == 0 || _notificationCache.Contains(notificationId))
    {
        notificationId = r.Next(1, 1000);
    }
    _notificationCache.Add(notificationId);
    //
                
    Notification.Builder builder = new Notification.Builder(Activity);
    builder.SetSmallIcon(Resource.Drawable.file_32x32)
        //устанавливаем заголовок для уведомления
        .SetContentTitle(filename)
        //в дескрипшне будет храниться наша строка &amp;quot;Загружаю файл на сервер...&amp;quot;
        .SetContentText(Activity.GetString(Resource.String.attachement_uploading))
        .SetAutoCancel(false)
        //запрещаем отмену уведомления пользователем
        .SetOngoing(true)
        //разрешаем уведомлению появиться в статус баре, мигать светодиодом и т.п.
        .SetDefaults(NotificationDefaults.All);

    NotificationManager manager = (NotificationManager)Activity.GetSystemService(Activity.NotificationService);
    manager.Notify(notificationId, builder.Build());

    //////////////////////////////////////////
    // ... код загрузки файла на сервер ... //
    //////////////////////////////////////////

    //отменяем уведомление и удаляем его ID из кеша
    manager.Cancel(notificationId);
    _notificationCache.Remove(notificationId);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
<pubDate>Sun, 20 Dec 2015 12:53:09 +0300</pubDate>
</item>


</channel>
</rss>