Da sowohl die neuen App-Erweiterungen von iOS 8 als auch Swift noch recht neu sind, habe ich eine Beispiel-App erstellt, die zeigt, wie eine einfache Heute-Erweiterung für iOS 8 in Swift erstellt werden kann. Dieses Widget zeigt die neuesten Blogbeiträge des Xebia Blogs in der Heute-Ansicht der Benachrichtigungszentrale an. Der Quellcode der App ist auf GitHub verfügbar. Die App ist nicht im App Store erhältlich, da es sich um eine Beispiel-App handelt, obwohl sie für alle, die diesen Blog verfolgen, recht nützlich sein könnte.
In diesem Tutorial werde ich die folgenden Schritte durchgehen:
-
- Erstellen Sie ein neues Projekt und fügen Sie eine Today Extension hinzu
- Verwenden Sie cocoapods, um nur dem Erweiterungsziel Abhängigkeiten hinzuzufügen, und verwenden Sie einen Bridging-Header, um diese Bibliotheken zu verwenden
- Laden Sie die Blog-Einträge aus dem RSS-Feed, wenn die Erweiterung eine Aktualisierung wünscht.
- Zeigen Sie die neuesten Artikel in einer Tabellenansicht mit Hilfe eines Storyboards an.
- Aktualisieren Sie die bevorzugte Inhaltsgröße des Widgets und stellen Sie sicher, dass die Tabellenansicht mit den Größenübergängen mitgeht.
- Öffnen Sie den Blogbeitrag in Safari, wenn Sie auf ein Element tippen
- Fügen Sie etwas Interaktivität hinzu, indem Sie mehr oder weniger Schaltflächen hinzufügen, um mehr oder weniger Artikel anzuzeigen.
- Zwischenspeichern der geladenen Artikel, um Artikel schnell anzuzeigen, wenn die Ansicht geladen wird, und um zu prüfen, ob sich Artikel nach einer Aktualisierungsanfrage geändert haben
Neues Xcode-Projekt
Obwohl eine Erweiterung für iOS 8 eine separate Binärdatei ist, ist es nicht möglich, eine Erweiterung ohne eine App zu erstellen. Das macht es leider unmöglich, eigenständige Widgets zu erstellen, was bei diesem Beispiel der Fall wäre, da es nur dazu dient, die neuesten Beiträge in der Heute-Ansicht anzuzeigen. Wir erstellen also ein neues Projekt in Xcode und implementieren eine sehr einfache Ansicht. Das Einzige, was die App jetzt tun wird, ist, den Benutzer anzuweisen, vom oberen Bildschirmrand nach unten zu wischen, um das Widget hinzuzufügen.
Es ist Zeit, unser Erweiterungsziel hinzuzufügen. Aus dem Menü Datei wählen wir Neues > Ziel... und aus der Anwendungserweiterung wählen wir Heute Erweiterung.
Wir werden unser Ziel XebiaBlogRSSWidget nennen und natürlich Swift als Sprache verwenden.
Das erstellte Ziel wird die folgenden Dateien enthalten:
-
-
- TodayViewController.swift
- MainInterface.storyboard
- Info.plist
-
Da wir einen Storyboard-Ansatz verwenden werden, ist diese Einstellung für uns in Ordnung. Wenn wir jedoch die Ansicht des Widgets programmatisch erstellen wollten, würden wir das Storyboard löschen und den Schlüssel NSExtensionMainStoryboard in der Info.plist durch NSExtensionPrincipalClass und TodayViewController als Wert ersetzen. Da (zum Zeitpunkt der Erstellung dieses Artikels) Xcode keine Swift-Klassen als Erweiterungs-Hauptklassen finden kann, müssten wir auch die folgende Zeile zu unserem TodayViewController hinzufügen: [objc] @objc (TodayViewController) [/objc] Update: Stellen Sie sicher, dass die Build-Einstellung "Embedded Content Contains Swift Code" des Hauptziels der App auf YES gesetzt ist. Andernfalls wird Ihr in Swift geschriebenes Widget abstürzen.
Abhängigkeiten mit Cocoapods hinzufügen
Das Widget bezieht die neuesten Blogbeiträge aus dem RSS-Feed des Blogs: xebia blog RSS feed/. Das bedeutet, dass wir etwas brauchen, das diesen Feed lesen und parsen kann. Eine Suche nach RSS bei Cocoapods liefert uns als erstes Ergebnis den BlockRSSParser. Er scheint genau das zu tun, was wir wollen. Wir brauchen also nicht weiter zu suchen und erstellen unser Podfile mit dem folgenden Inhalt: [ruby] platform :ios, "8.0" target "XebiaBlog" do end target "XebiaBlogRSSWidget" do pod 'BlockRSSParser', '~> 2.1' end [/ruby] Es ist wichtig, die Abhängigkeit nur zum Ziel XebiaBlogRSSWidget hinzuzufügen, da Xcode zwei Binärdateien erstellt, eine für die App selbst und eine separate für das Widget. Wenn wir die Abhängigkeit zu allen Zielen hinzufügen würden, wäre sie in beiden Binärdateien enthalten und würde somit die Gesamtgröße des Downloads für unsere App erhöhen. Fügen Sie immer nur die notwendigen Abhängigkeiten sowohl zu Ihrem App-Ziel als auch zu Ihrem Widget-Ziel(en) hinzu. Hinweis: Cocoapods oder Xcode können Ihnen Probleme bereiten, wenn Sie ein Ziel ohne Pod-Abhängigkeiten haben. In diesem Fall können Sie eine Abhängigkeit zu Ihrem Hauptziel hinzufügen und pod install ausführen. Danach können Sie die Abhängigkeit möglicherweise wieder löschen. Der BlockRSSParser ist in Objective-C geschrieben, was bedeutet, dass wir einen Objective-C-Bridging-Header hinzufügen müssen, um ihn von Swift aus verwenden zu können. Wir fügen die Datei XebiaBlogRSSWidget-Bridging-Header.h zu unserem Ziel hinzu und fügen den Import hinzu.
[objc]
#import "RSSParser.h"
[/objc]
Außerdem müssen wir dies dem Swift-Compiler in unseren Build-Einstellungen mitteilen:
RSS-Feed laden
Endlich ist es an der Zeit, etwas zu kodieren. Der generierte TodayViewController hat eine Funktion namens widgetPerformUpdateWithCompletionHandler. Diese Funktion wird ab und zu aufgerufen, um nach neuen Daten zu fragen. Sie wird auch direkt nach viewDidLoad aufgerufen, wenn das Widget angezeigt wird. Die Funktion hat einen Completion Handler als Parameter, den wir aufrufen müssen, wenn wir mit dem Laden der Daten fertig sind. Ein Completion Handler wird anstelle einer Rückgabefunktion verwendet, damit wir unseren Feed asynchron laden können. In Objective-C würden wir den folgenden Code schreiben, um unseren Feed zu laden:
[objc]
[RSSParser parseRSSFeedForRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@" https://xebia.com/blog/feed/ "]] success:^(NSArray *feedItems) {
// success
} failure:^(NSError *error) {
// failure
}];
[/objc]
In Swift sieht dies etwas anders aus. Hier die vollständige Implementierung von widgetPerformUpdateWithCompletionHandler:
[objc]
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
let url = NSURL(string: " https://xebia.com/blog/feed/ ")
let req = NSURLRequest(URL: url)
RSSParser.parseRSSFeedForRequest(req,
success: { feedItems in
self.items = feedItems as? [RSSItem]
completionHandler(.NewData)
},
failure: { error in
println(error)
completionHandler(.Failed)
})
}
[/objc]
Wir weisen das Ergebnis einer neuen optionalen Variablen vom Typ RSSItem-Array zu:
[objc]
var items : [RSSItem]?
[/objc]
Der Completion Handler wird entweder mit NCUpdateResult.NewData aufgerufen, wenn der Aufruf erfolgreich war oder mit NCUpdateResult.Failed, wenn der Aufruf fehlgeschlagen ist. Eine dritte Option ist NCUpdateResult.NoData, die verwendet wird, um anzuzeigen, dass es keine neuen Daten gibt. Darauf kommen wir später in diesem Beitrag zu sprechen, wenn wir unsere Daten zwischenspeichern.
Elemente in einer Tabellenansicht anzeigen
Jetzt, da wir unsere Artikel aus dem RSS-Feed abgerufen haben, können wir sie in einer Tabellenansicht anzeigen. Wir ersetzen unseren normalen View Controller durch einen Table View Controller in unserem Storyboard und ändern die Superklasse von TodayViewController und fügen der Prototypzelle drei Beschriftungen hinzu. Das ist nicht anders als in iOS 7, daher werde ich hier nicht zu sehr ins Detail gehen (das vollständige Projekt finden Sie auf GitHub). Wir erstellen auch eine neue Swift-Klasse für unsere benutzerdefinierte Unterklasse Table View Cell und erstellen Ausgänge für unsere 3 Labels.
[objc]
import UIKit
class RSSItemTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var authorLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
}
[/objc]
Jetzt können wir unsere Funktionen für die Datenquelle der Tabellenansicht implementieren.
[objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return items.count
}
return 0
}
[/objc]
Da items eine optionale Variable ist, verwenden wir die Optionale Bindung, um zu prüfen, dass sie nicht null ist, und weisen sie dann einer temporären nicht optionalen Variablen zu: let items. Es ist in Ordnung, wenn Sie der temporären Variablen denselben Namen wie der Klassenvariablen geben.
[objc]
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RSSItem", forIndexPath: indexPath) as RSSItemTableViewCell
if let item = items?[indexPath.row] {
cell.titleLabel.text = item.title
cell.authorLabel.text = item.author
cell.dateLabel.text = dateFormatter.stringFromDate(item.pubDate)
}
return cell
}
[/objc]
In unserem Storyboard haben wir den Typ der Prototypzelle auf unsere benutzerdefinierte Klasse RSSItemTableViewCell festgelegt und RSSItem als Bezeichner verwendet, so dass wir hier eine Zelle als RSSItemTableViewCell dequeuen können, ohne befürchten zu müssen, dass sie nicht vorhanden ist. Anschließend verwenden wir Optional Binding, um das Element an unserem Zeilenindex zu erhalten. Wir könnten auch erzwungenes Unwrapping verwenden, da wir sicher wissen, dass items hier nicht null ist:
[objc]
let item = items![indexPath.row]
[/objc]
Aber die optionale Bindung macht unseren Code sicherer und verhindert einen zukünftigen Absturz, falls sich unser Code ändern sollte. Wir müssen auch den Datumsformatierer erstellen, den wir oben verwenden, um die Veröffentlichungsdaten in den Zellen zu formatieren:
[objc]
let dateFormatter : NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateStyle = .ShortStyle
return formatter
}()
[/objc]
Hier verwenden wir eine Schließung, um den Datumsformatierer zu erstellen und ihn mit unserem bevorzugten Datumsstil zu initialisieren. Der Rückgabewert der Schließung wird dann der Eigenschaft zugewiesen.
Bevorzugte Größe des Inhalts
Um sicherzustellen, dass wir die Tabellenansicht tatsächlich sehen können, müssen wir die bevorzugte Inhaltsgröße des Widgets festlegen. Wir fügen unserer Klasse eine neue Funktion hinzu, die dies tut.
[objc]
func updatePreferredContentSize() {
preferredContentSize = CGSizeMake(CGFloat(0), CGFloat(tableView(tableView, numberOfRowsInSection: 0)) * CGFloat(tableView.rowHeight) + tableView.sectionFooterHeight)
}
[/objc]
Da die Widgets alle eine feste Breite haben, können wir für die Breite einfach 0 angeben. Die Höhe wird berechnet, indem die Anzahl der Zeilen mit der Höhe der Zeilen multipliziert wird. Da dadurch die bevorzugte Höhe größer als die maximal zulässige Höhe eines Heute-Widgets ist, wird es automatisch verkleinert. Wir fügen auch die
[objc]
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition({ context in
self.tableView.frame = CGRectMake(0, 0, size.width, size.height)
}, completion: nil)
}
[/objc]
Hier setzen wir einfach die Größe der Tabellenansicht auf die Größe des Widgets, das der erste Parameter ist. Natürlich müssen wir noch unsere Update-Methode sowie reloadData auf unserer tableView aufrufen. Wir fügen also diese beiden Aufrufe zu unserer Success Closure hinzu, wenn wir die Elemente aus dem Feed laden
[objc]
success: { feedItems in
self.items = feedItems as? [RSSItem]
self.tableView.reloadData()
self.updatePreferredContentSize()
completionHandler(.NewData)
},
[/objc]
Lassen Sie unser Widget laufen:
Es funktioniert, aber wir können es besser aussehen lassen. Tabellenansichten haben standardmäßig eine weiße Hintergrundfarbe und eine schwarze Textfarbe und das ist in einem Heute-Widget nicht anders. Wir möchten den Stil an das standardmäßige iOS Heute-Widget anpassen, also geben wir der Tabellenansicht einen klaren Hintergrund und machen den Text der Beschriftungen weiß. Leider werden unsere Beschriftungen dadurch praktisch unsichtbar, da der Storyboard-Editor in Xcode bei Ansichten mit einer klaren Hintergrundfarbe immer noch einen weißen Hintergrund anzeigt. Wenn wir das Ganze noch einmal ausführen, erhalten wir ein viel besser aussehendes Ergebnis:
Beitrag in Safari öffnen
Um einen Blogbeitrag in Safari zu öffnen, wenn Sie auf ein Element tippen, müssen wir die Funktion tableView:didSelectRowAtIndexPath: implementieren. In einer normalen App würden wir dann die openURL: Methode von UIApplication verwenden. Aber das ist in einer Today-Erweiterung nicht möglich. Stattdessen müssen wir die openURL:completionHandler: Methode von NSExtensionContext verwenden. Wir können diesen Kontext über die Eigenschaft extensionContext unseres View Controllers abrufen.
[objc]
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let item = items?[indexPath.row] {
if let context = extensionContext {
context.openURL(item.link, completionHandler: nil)
}
}
}
[/objc]
Mehr und weniger Tasten
Im Moment nimmt unser Widget etwas zu viel Platz in der Benachrichtigungszentrale ein. Lassen Sie uns dies ändern, indem wir standardmäßig nur 3 Elemente und maximal 6 Elemente anzeigen. Das Umschalten zwischen dem Standard- und dem erweiterten Zustand kann über eine Schaltfläche erfolgen, die wir in der Fußzeile der Tabellenansicht einfügen. Wenn der Benutzer das Notification Center schließt und wieder öffnet, möchten wir es in demselben Zustand anzeigen, in dem es vorher war. Hierfür können wir NSUserDefaults verwenden. Die Verwendung einer berechneten Eigenschaft zum Lesen und Schreiben aus den Benutzereinstellungen ist eine gute Möglichkeit, dies zu schreiben:
[objc]
let userDefaults = NSUserDefaults.standardUserDefaults()
var expanded : Bool {
get {
return userDefaults.boolForKey("expanded")
}
set (newExpanded) {
userDefaults.setBool(newExpanded, forKey: "expanded")
userDefaults.synchronize()
}
}
[/objc]
So können wir sie wie jede andere Eigenschaft verwenden, ohne zu bemerken, dass sie in den Benutzervorgaben gespeichert wird. Wir fügen auch Variablen für unsere Schaltfläche und die Anzahl der Standard- und Maximalzeilen hinzu:
[objc]
let expandButton = UIButton()
let defaultNumRows = 3
let maxNumberOfRows = 6
[/objc]
Anhand des aktuellen Werts der erweiterten Eigenschaft bestimmen wir die Anzahl der Zeilen, die unsere Tabellenansicht haben soll. Natürlich sollten nie mehr als die tatsächlich vorhandenen Elemente angezeigt werden, also berücksichtigen wir auch das und ändern unsere Funktion in die folgende:
[objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return min(items.count, expanded ? maxNumberOfRows : defaultNumRows)
}
return 0
}
[/objc]
Dann den Code, damit unsere Schaltfläche funktioniert:
[objc]
override func viewDidLoad() {
super.viewDidLoad()
updateExpandButtonTitle()
expandButton.addTarget(self, action: "toggleExpand", forControlEvents: .TouchUpInside)
tableView.sectionFooterHeight = 44
}
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return expandButton
}
[/objc]
Je nach dem aktuellen Wert unserer erweiterten Eigenschaft wird entweder "Weniger anzeigen" oder "Mehr anzeigen" als Titel der Schaltfläche angezeigt.
[objc]
func updateExpandButtonTitle() {
expandButton.setTitle(expanded ? "Show less" : "Show more", forState: .Normal)
}
[/objc]
Wenn wir auf die Schaltfläche tippen, kehren wir die erweiterte Eigenschaft um, aktualisieren den Schaltflächentitel und die bevorzugte Inhaltsgröße und laden die Daten der Tabellenansicht neu.
[objc]
func toggleExpand() {
expanded = !expanded
updateExpandButtonTitle()
updatePreferredContentSize()
tableView.reloadData()
}
[/objc]
Und als Ergebnis können wir nun die Anzahl der Zeilen, die wir sehen möchten, umschalten.
Caching
Im Moment erhalten wir jedes Mal, wenn wir das Widget öffnen, zunächst eine leere Liste und dann, sobald der Feed geladen ist, werden die Artikel angezeigt. Um dies zu verbessern, können wir die abgerufenen Artikel zwischenspeichern und diese anzeigen, sobald das Widget geöffnet wird, bevor wir die Artikel aus dem Feed laden. Die TMCache-Bibliothek macht dies mit wenig Aufwand möglich. Wir können sie zu unserer Pods-Datei und unserem Bridging-Header hinzufügen, so wie wir es bei der BlockRSSParser-Bibliothek getan haben. Auch hier funktioniert eine berechnete Eigenschaft gut, um die Elemente zwischenzuspeichern und die eigentliche Implementierung zu verbergen:
[objc]
var cachedItems : [RSSItem]? {
get {
return TMCache.sharedCache().objectForKey("feed") as? [RSSItem]
}
set (newItems) {
TMCache.sharedCache().setObject(newItems, forKey: "feed")
}
}
[/objc]
Da die RSSItem-Klasse der BlockRSSParser-Bibliothek mit dem NSCoding-Protokoll konform ist, können wir sie direkt mit TMCache verwenden. Wenn wir die Elemente zum ersten Mal aus dem Cache abrufen, erhalten wir den Wert Null, da der Cache leer ist. Daher muss cachedItems ein optionales Element sein, ebenso wie der Downcast und wir müssen den as? Operator verwenden. Wir können den Cache nun aktualisieren, sobald die Elemente geladen sind, indem wir der Eigenschaft einen Wert zuweisen. Also fügen wir in unserer Success Closure Folgendes hinzu:
[objc]
self.cachedItems = self.items
[/objc]
Und um die zwischengespeicherten Elemente zu laden, fügen wir zwei weitere Zeilen am Ende von viewDidLoad hinzu:
[objc]
items = cachedItems
updatePreferredContentSize()
[/objc]
Und schon sind wir fertig. Jetzt werden bei jedem Öffnen des Widgets zuerst die zwischengespeicherten Elemente angezeigt. Es gibt noch eine letzte Sache, mit der wir unser Widget verbessern können. Wie bereits erwähnt, kann der completionHandler von widgetPerformUpdateWithCompletionHandler auch mit NCUpdateResult.NoData aufgerufen werden. Da wir nun die zuvor geladenen Elemente haben, können wir die neu geladenen Elemente mit den alten vergleichen und NoData verwenden, falls sie sich nicht geändert haben. Hier ist unsere endgültige Implementierung des Erfolgsabschlusses:
[objc]
success: { feedItems in
if self.items == nil || self.items! != feedItems {
self.items = feedItems as? [RSSItem]
self.tableView.reloadData()
self.updatePreferredContentSize()
self.cachedItems = self.items
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
},
[/objc]
Und da es sich um Swift handelt, können wir einfach den Operator != verwenden, um festzustellen, ob die Arrays ungleiche Inhalte haben.
Quellcode auf GitHub
Wie zu Beginn dieses Beitrags erwähnt, ist der Quellcode des Projekts auf GitHub verfügbar, mit einigen kleineren Änderungen, die für diesen Blogbeitrag nicht wesentlich sind. Natürlich sind Pull Requests immer willkommen. Lassen Sie mich außerdem in den Kommentaren unten wissen, ob Sie dieses Widget im App Store veröffentlicht sehen möchten.
Verfasst von

Lammert Westerhoff
Contact



