Расширение Today в iOS Swift 4

Today extension in iOS

Для удобства использования мобильного приложения в iOS существуют расширения, которые предоставляют быстрый доступ к информации. При этом не нужно открывать приложение полностью, достаточно лишь на основном экране системы смахнуть влево и вы увидите список виджетов — это и есть Today extension, о которых мы и поговорим сегодня.

Today extension в iOS может существовать только если у вас есть основное приложение.

Здесь может отображаться информация из базы данных основного приложения, картинки которые ранее были загружены в файловую систему, подгружаться информация по сети также есть удобная возможность быстрого перехода в основное приложение.

Как добавить Today Extension

Today Extension, как и говорилось ранее, могут быть добавлены в уже существующий проект, поэтому если у вас его еще нет — нужно создать.

Итак, у вас уже есть проект, пришло время добавить Today Extension. Для этого нужно перейти в меню File > New > Target:

После этого в списке таргетов найти Today Extension и нажать Next:

Далее, вам предложат ввести имя, ваши данные, выбрать проект и приложение-контейнер, к которому будет привязано ваше Today Extension.

После нажатия кнопки Finish потребуется активировать схему данного таргета для возможности его билда и запуска.

В списке файлов нашего проекта появилась папка с названием вашего today extension. В ней по умолчанию есть TodayViewController который реализует протокол NCWidgetProviding, MainInterface.storyboard, в котором можно реализовать ваш UI для виджета и файл настроек Info.plist.

Теперь мы можем запустить наше приложение — перейти к панели виджетов, смахнув на главном экране влево, и увидеть наш виджет. Если он еще не отображается, то нажать на кнопку «Изменить» и добавить его.

Высота Today Extension

У виджета есть функционал, который позволяет раскрыть его и посмотреть более подробную информацию. Для этого нужно, например, во viewDidLoad() задать widgetLargestAvailableDisplayMode равен expanded:

    override func viewDidLoad() {
        super.viewDidLoad()
        self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded
    }

После этого у виджета появится кнопка «Больше/Меньше» в правом верхнем углу:

Но работать она не будет пока вы не реализуете метод протокола NCWidgetProviding:

func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
        if activeDisplayMode == .compact {
            self.preferredContentSize = maxSize
        } else if activeDisplayMode == .expanded {
            self.preferredContentSize = CGSize(width: maxSize.width, height: 150)
        }
    }

Этот метод будет вызываться каждый раз, когда вы нажимаете на кнопку «Больше/Меньше». На данный момент activeDisplayMode может быть равен compact или expanded.

  • compact WidgetDisplayMode — свернутый режим (показана кнопка «Больше»). Высота Today Extension составляет 110 (без возможности изменения) при условии, что системный шрифт выставлен по умолчанию, если же он будет изменен то, соответственно, и высота виджета будет варьироваться;
  • expanded WidgetDisplayMode — развернутый режим (показана кнопка «Меньше»). Высота Today Extension может составлять от 110 до 440 и, возможно, при смене системного шрифта высота виджета также будет варьироваться.

В зависимости от activeDisplayMode мы задаем значение свойству self.preferredContentSize которое требуется. Например, для таблицы это может быть высота ячейки, умноженная на количество столбцов (для отображения всех данных сразу), но не забываем про ограничение maxSize.

Интерфейс Today Extension

Для создания интерфейса мы можем использовать storyboard или добавлять элементы в коде в TodayViewController, всё так же, как и при создании UIViewController для основного приложения.

Чтобы виджет всегда выглядел актуальным, iOS записывает снапшоты последнего состояния виджета, перед тем как он уйдет с экрана. Когда виджет снова становится видимым, сначала отображается снапшот, а лишь потом актуальная информация. Чтобы виджет обновил свое состояние перед снапшотом, используется протокол NCWidgetProviding.

Когда у виджета вызывается widgetPerformUpdateWithCompletionHandler, он должен обновить свое окно и после вызвать блок completionHandler с аргументом, равным одной из следующих констант:

  • NCUpdateResultNewData — новый контент требует обновления окна;
  • NCUpdateResultNoData — виджету не требуется обновление;
  • NCUpdateResultFailed — во время обновления произошла ошибка.

Для примера добавим два UIlabel через storyboard и сделаем анимацию для одного из них.

Прописываем IBOutlets в TodayViewController:

@IBOutlet weak var textLabel:UILabel!
@IBOutlet weak var dateLabel:UILabel!

И соединяем их в storyboard с UIlabel которые были уже добавлены с помощью constraints:

Далее, сделаем метод анимации textLabel, в зависимости от текущего activeDisplayMode:

    func animateTextLabels() {
        let isExpandedMode = self.extensionContext?.widgetActiveDisplayMode == .expanded
        let scaleText:CGFloat = isExpandedMode ? 3 : 0.3
        UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
            self.textLabel.transform = .init(scaleX: scaleText, y: scaleText)
            self.dateLabel.transform = isExpandedMode ? .init(translationX: 0, y: 20) : .identity
        }) { (finished) in
            UIView.animate(withDuration: 0.3, animations: {
                self.textLabel.transform = .identity
            })
        }
    }

И будем вызывать его при нажатии кнопки «Больше/Меньше»:

    func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
        animateTextLabels()
        if activeDisplayMode == .compact {
            self.preferredContentSize = maxSize
        } else if activeDisplayMode == .expanded {
            self.preferredContentSize = CGSize(width: maxSize.width, height: 150)
        }
    }

Запустим проект, перейдем в панель где отображаются виджеты и посмотрим что получилось. Это простейший пример, для того чтобы показать возможности работы с UI элементами виджета.

UserDefaults в Today Extension

Если у вас данные статические или же берутся из Foundation (например Date) то всё просто. Но если нужно вывести данные, которые были записаны из главного приложения, тогда требуется создавать для них контейнер (группу) для таргетов.

Добавляем группу со своим названием и активируем её. В таргете виджета группа тоже появится, её нужно активировать.

После того как контейнер создан, можно записать данные, например, используем UserDefault и название группы:

let sharedDefaults = UserDefaults(suiteName: "group.sharingForTodayExtension")
sharedDefaults?.setValue("Stfalcon.com today extension tutorial", forKey: "customKey")

Чтобы эти данные отобразить в виджете потребуется просто обратиться к UserDefaults с таким же suiteName:

let sharedDefaults = UserDefaults.init(suiteName: "group.sharingForTodayExtension")
 let text = sharedDefaults?.value(forKey: "customKey")

CoreData в Today Extension

Для использования coredata также требуется работа через контейнер, но в этом случае по контейнеру будет работа с файлом базы данных. Чтобы NSPersistentContainer считывал и записывал данные в общий контейнер нужно создать его наследника и переопределить некоторые методы:

 class PersistentContainer: NSPersistentContainer{
    override class func defaultDirectoryURL() -> URL{
        return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.sharingForTodayExtension")!
    }
 
    override init(name: String, managedObjectModel model: NSManagedObjectModel) {
        super.init(name: name, managedObjectModel: model)
    }
}

А также нужно поставить галочку возле таргета нашего виджета, чтобы он имел доступ к модели CoreData.

Теперь можно использовать новый PersistentContainer и работать с ним как в приложении, так и в виджете.

Открываем приложение с помощью Today Extension

Для это нужно открыть url со схемой вашего основного приложения-контейнера:

   let url = URL(string: "mainAppUrl://")!
        self.extensionContext?.open(url, completionHandler: { (success) in
            if (!success) {
                print("error: failed to open app from Today Extension")
            }
        })

В url можно указать, например, id или название объекта, по которому программа-контейнер, при условии реализации логики, «узнает», какой ViewController, какой запрос отправить на сервер и т.д.

Если у приложения-контейнера еще нету своей url схемы, можно добавить её:

Компания Stfalcon.com более 8-ми лет занимается реализацией сложных решений для среднего и крупного бизнеса. Хотите заказать мобильное приложение, систему для автоматизации логистики или другое сложное облачное решение? Поделитесь с нами вашей идеей на info@stfalcon.com. Мы создадим уникальный проект для вас!