Розширення 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. Ми створимо унікальний проект для Вас!