内容简介:As soon as I saw the new home screen widgets in this year’s WWDC Platforms State of the Union, I knew I had to make one for my favorite app! It would be so great to see it right there on the home screen. And I’m not alone. Everyone’s doing it! Apple knows
As soon as I saw the new home screen widgets in this year’s WWDC Platforms State of the Union, I knew I had to make one for my favorite app! It would be so great to see it right there on the home screen. And I’m not alone. Everyone’s doing it! Apple knows it’s on a winner and provided a three-part code-along to get everyone started.
There are several how-tos already published, so what’s different about this tutorial? Well, I decided to add a widget to a sizable SwiftUI app written by a team of developers — and none of them was me. There’s a huge amount of code to sift through to find what I need to build my widget. And none of it was written with widget-making in mind. So follow along with me as I show you how I did it.
: You’ll need Xcode 12 beta. You’ll also need an iOS device running iOS 14. Catalina is OK. If you have a Mac [partition] running the Big Sur beta, you could try running your code there, in case it’s not working on Catalina.
Most importantly, this is a truly bleeding-edge API at the moment. Things that appeared in the WWDC demos are not part of Xcode 12 beta 1. You are likely to experience some instability. That said, Widgets are cool and a ton of fun!
Getting Started
Download the project materials using the Download Materials
button at the top or bottom of this tutorial. Before you open the starter project, open Terminal, cd
to the starter/emitron-iOS-development
folder, then run this command:
scripts/generate_secrets.sh
You’re generating some secrets files needed to run the project.
Now open the Emitron project in the starter/emitron-iOS-development folder. This takes a while to fetch some packages, so here’s some information about the project while you wait.
Emitron is the raywenderlich.com app . If you’re a subscriber, you’ve surely installed it on your iPhone and iPad. It lets you stream videos and, if you have a Professional subscription, you can download videos for off-line playback.
The project is open source. You can read about it in its GitHub repository , and you’re certainly welcome to contribute to improving it.
The starter version you’ve downloaded has a few modifications:
- Settings are up-to-date and the iOS Deployment Target is 14.0.
-
In Downloads/DownloadService.swift
, two
promise
statements that caused errors in Xcode beta 1 are commented out. The download service is for the Professional subscription, and you don’t need it for this tutorial. -
In Guardpost/Guardpost.swift
,
authSession?.prefersEphemeralWebBrowserSession
is set tofalse
, which avoids the need to enter login details every time you build and run the app. You still have to tap Sign in , which prompts you to use raywenderlich.com to sign in. Tap Continue . The first time you build and run, you might still have to enter your email and password, but in subsequent build and runs, tapping Continue skips the login form.
By now, Xcode has installed all the packages. Build and run in a simulator.
Emitron running in simulator.
Ignore the warnings about Hashable
and SwiftyJSON. Scrolling and playback don’t work well in Xcode beta 1, but you won’t be fixing that in this tutorial. If you scroll “too much”, the app crashes. Also not your problem. ;]
WidgetKit
This tutorial is all about adding a shiny new widget to Emitron.
Adding a Widget Extension
Start by adding a widget extension with File ▸ New ▸ Target… .
Create a new target.
Search for “widget”, select Widget Extension and click Next :
Search for “widget”.
Name it EmitronWidget and make sure Include Configuration Intent is not checked:
Don’t select Include Configuration Intent.
There are two widget configurations: Static
and Intent
. A widget with IntentConfiguration
uses Siri intents to let the user customize widget parameters.
Click Finish and agree to the activate-scheme dialog:
Activate scheme for new widget extension.
Running Your Widget
The widget template provides a lot of boilerplate code you just have to customize. It works right out of the box, so you can set up everything now to make sure everything runs smoothly when you’re ready to test your code.
Note : The Xcode 12 beta 1 simulator doesn’t display your widget in the widget gallery. So, until some future beta version, you must build and run on an iOS 14 device. If you don’t have a device available, you can run the Widget scheme instead of the main Emitron scheme, and the widget will appear on the simulator home screen.
In the Project navigator, select the top level Emitron folder to sign your targets. Change the bundle identifiers and set the team for every version of every target.
Note : For the widget, you might encounter an apparent Xcode bug that sets the signing identity to Distribution for two of the three versions. If you see this error, open Build Settings , search for distribution and change the signing identity to Apple Development .
One last gotcha: Make sure your widget’s bundle ID prefix matches the app’s. This means you will need to insert dev between “ios” and “EmitronWidget” to get your.prefix.emitron.ios.dev.EmitronWidget .
OK, now connect your iOS device, select the Emitron scheme and your device, then build and run. Sign in, then close the app and press on some empty area of your home window until the icons start to jiggle.
Tap the + button in the upper right corner, then scroll down to find raywenderlich :
Scroll down in widget gallery.
Select it to see snapshots of the three sizes:
Snapshots of the three widget sizes.
Tap Add Widget to see your widget on the screen:
Your widget on the home screen.
Tap the widget to reopen Emitron.
Your widget works! Now, you simply have to make it display information from Emitron.
Defining Your Widget
It makes sense to make your widget display some of the information the app shows for each tutorial.
Card view in the Emitron app.
This view is defined in UI/Shared/Content List/CardView.swift . My first idea was to just add the widget target to this file. But that required adding more and more and more files, to accommodate all the intricate connections in Emitron.
All you really need are the Text
views. The images are cute, but you’d need to include the persistence infrastructure to keep them from disappearing.
You’re going to copy the layout of the relevant Text
views. These use several utility extensions, so find these files and add the EmitronWidgetExtension
target to them:
Add the widget target to these files.
Note : Be sure you notice Assets at the top of the image.
CardView displays properties of a ContentListDisplayable
object. This is a protocol defined in Displayable/ContentDisplayable.swift
:
protocol ContentListDisplayable: Ownable { var id: Int { get } var name: String { get } var cardViewSubtitle: String { get } var descriptionPlainText: String { get } var releasedAt: Date { get } var duration: Int { get } var releasedAtDateTimeString: String { get } var parentName: String? { get } var contentType: ContentType { get } var cardArtworkUrl: URL? { get } var ordinal: Int? { get } var technologyTripleString: String { get } var contentSummaryMetadataString: String { get } var contributorString: String { get } // Probably only populated for screencasts var videoIdentifier: Int? { get } }
Your widget only needs name
, cardViewSubtitle
, descriptionPlainText
and releasedAtDateTimeString
. So you’ll create a struct for these properties.
Creating a TimelineEntry
Create a new Swift file named WidgetContent.swift and make sure its targets are emitron and EmitronWidgetExtension :
Create WidgetContent with targets emitron and widget.
It should be in the EmitronWidget group.
Now add this code to your new file:
import WidgetKit struct WidgetContent: TimelineEntry { var date = Date() let name: String let cardViewSubtitle: String let descriptionPlainText: String let releasedAtDateTimeString: String }
To use WidgetContent
in a widget, it must conform to TimelineEntry
. The only required property is date
, which you initialize to the current date.
Creating an Entry View
Next, create a view to display the four String
properties. Create a new SwiftUI View file
and name it EntryView.swift
. Make sure its target is only EmitronWidgetExtension
, and it should also be in the EmitronWidget
group:
Create EntryView with only the widget as target.
Now replace the contents of struct EntryView
with this code:
let model: WidgetContent var body: some View { VStack(alignment: .leading) { Text(model.name) .font(.uiTitle4) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) .padding([.trailing], 15) .foregroundColor(.titleText) Text(model.cardViewSubtitle) .font(.uiCaption) .lineLimit(nil) .foregroundColor(.contentText) Text(model.descriptionPlainText) .font(.uiCaption) .fixedSize(horizontal: false, vertical: true) .lineLimit(2) .lineSpacing(3) .foregroundColor(.contentText) Text(model.releasedAtDateTimeString) .font(.uiCaption) .lineLimit(1) .foregroundColor(.contentText) } .background(Color.cardBackground) .padding() .cornerRadius(6) }
You’re essentially copying the Text
views from CardView
and adding padding.
Delete EntryView_Previews
entirely.
Creating Your Widget
Now start defining your widget. Open EmitronWidget.swift
and double-click
SimpleEntry
in the line:
public typealias Entry = SimpleEntry
Choose Editor ▸ Edit All in Scope and change the name to WidgetContent . This will cause several errors, which you’ll fix in the next few steps. First delete the declaration:
struct WidgetContent: TimelineEntry { public let date: Date }
This declaration is now redundant and conflicts with the one in WidgetContent.swift .
Creating a Snapshot Entry
One of the provider’s methods provides a snapshot entry to display in the widget gallery. You’ll use a specific WidgetContent
object for this.
Just below the import
statements, add this global object:
let snapshotEntry = WidgetContent( name: "iOS Concurrency with GCD and Operations", cardViewSubtitle: "iOS & Swift", descriptionPlainText: """ Learn how to add concurrency to your apps! \ Keep your app's UI responsive to give your \ users a great user experience. """, releasedAtDateTimeString: "Jun 23 2020 • Video Course (3 hrs, 21 mins)")
This is the update to our concurrency video course, which was published on WWDC day 2.
Now replace the first line of snapshot(with:completion:)
with this:
let entry = snapshotEntry
When you view this widget in the gallery, it will display this entry.
Creating a Temporary Timeline
A widget needs a TimelineProvider
to feed it entries of type TimelineEntry
. It displays each entry at the time specified by the entry’s date
property.
The most important provider method is timeline(with:completion:)
. It already has some code to construct a timeline, but you don’t have enough entries yet. So comment out all but the last two lines, and add this line:
let entries = [snapshotEntry]
You’re creating an entries
array that contains just your snapshotEntry
.
Creating a Placeholder View
The widget displays its PlaceholderView
while it’s waiting for actual timeline entries. You’ll use snapshotEntry
for this, too.
Replace the Text
view with this:
EntryView(model: snapshotEntry)
The WWDC code-along showed a special modifier that blurs out the view’s content, to make it clear that this is a placeholder, not the real thing. It’s this:
.isPlaceholder(true)
It looks very cool in the WWDC video, but it doesn’t compile in Xcode 12 beta 1. See this entry in the Apple Developer Forums for more information.
Defining Your Widget
Finally, you can put all these parts together.
First, delete EmitronWidgetEntryView
. You’ll use your EntryView
instead.
Now replace the internals of EmitronWidget
with the following:
private let kind: String = "EmitronWidget" public var body: some WidgetConfiguration { StaticConfiguration( kind: kind, provider: Provider(), placeholder: PlaceholderView() ) { entry in EntryView(model: entry) } .configurationDisplayName("RW Tutorials") .description("See the latest video tutorials.") }
The three strings are whatever you want: kind
describes your widget, and the last two strings appear above each widget size in the gallery.
Build and run on your device, sign in, then close the app to see your widget.
If it’s still displaying the time, delete it and add it again.
Widget gallery with snapshot entry.
And here’s what the medium size widget looks like now:
The medium size widget on the home screen.
Only the medium size widget looks OK, so modify your widget to provide only that size. Add this modifier below .description
:
.supportedFamilies([.systemMedium])
Next, you’ll provide real entries for your timeline, directly from the app’s repository!
Providing Timeline Entries
The app displays the array of ContentListDisplayable
objects in contents
, created in Data/ContentRepositories/ContentRepository.swift
. To share this information with your widget, you’ll create an app group. Then, in ContentRepository.swift
, you’ll write a file to this app group, which you’ll read from in EmitronWidget.swift
.
Creating an App Group
On the project page, select the emitron target. In the Signing & Capabilities tab, click + Capability , then drag App Group into the window. Name it group.your.prefix.emitron.contents ; be sure to replace your.prefix appropriately.
Now select the EmitronWidgetExtension target and add the App Group capability. Scroll through the App Groups to find and select group.your.prefix.emitron.contents .
Writing the Contents File
At the top of ContentRepository.swift
, just below the import Combine
statement, add this code:
import Foundation extension FileManager { static func sharedContainerURL() -> URL { return FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: "group.your.prefix.emitron.contents" )! } }
This is just some standard code for getting the app group container’s URL. Be sure to substitute your app identifier prefix.
Now, just below var contents
, add this helper method:
func writeContents() { let widgetContents = contents.map { WidgetContent(name: $0.name, cardViewSubtitle: $0.cardViewSubtitle, descriptionPlainText: $0.descriptionPlainText, releasedAtDateTimeString: $0.releasedAtDateTimeString) } let archiveURL = FileManager.sharedContainerURL() .appendingPathComponent("contents.json") print(">>> \(archiveURL)") let encoder = JSONEncoder() if let dataToSave = try? encoder.encode(widgetContents) { do { try dataToSave.write(to: archiveURL) } catch { print("Error: Can't write contents") return } } }
Here, you create an array of WidgetContent
objects, one for each item in the repository. You convert each to JSON and save it to the app group’s container.
Set a breakpoint at the let archiveURL
line.
You’ll call this method when contents
is set. Add this didSet
closure to contents
:
didSet { writeContents() }
If Xcode is on its toes, it’s complaining about WidgetContent
. Jump to the definition of WidgetContent
and make it conform to Codable
:
struct WidgetContent: Codable, TimelineEntry {
Now build and run the app in a simulator. At the breakpoint, widgetContents
has 20 values.
Continue program execution and scroll down in the app. At the breakpoint, widgetContents
now has 40 values. So you have some control over how many items you share with your widget.
Stop the app, disable the breakpoint, then copy the URL folder path from the debug console and locate in in Finder . Take a look at contents.json .
Next, go and set up the widget to read this file.
Reading the Contents File
In EmitronWidget.swift
, add the same FileManager
code:
extension FileManager { static func sharedContainerURL() -> URL { return FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: "group.your.prefix.emitron.contents" )! } }
Be sure to update your prefix.
Add this helper method to Provider
:
func readContents() -> [Entry] { var contents: [WidgetContent] = [] let archiveURL = FileManager.sharedContainerURL() .appendingPathComponent("contents.json") print(">>> \(archiveURL)") let decoder = JSONDecoder() if let codeData = try? Data(contentsOf: archiveURL) { do { contents = try decoder.decode([WidgetContent].self, from: codeData) } catch { print("Error: Can't decode contents") } } return contents }
This reads the file you saved into the app group’s container.
Uncomment the code in timeline(with:completion:)
, then replace this line:
var entries: [WidgetContent] = []
With
var entries = readContents()
Next, modify the comment and for
loop to add dates to your entries:
// Generate a timeline by setting entry dates interval seconds apart, // starting from the current date. let currentDate = Date() let interval = 5 for index in 0 ..< entries.count { entries[index].date = Calendar.current.date(byAdding: .second, value: index * interval, to: currentDate)! }
Delete the let entries
line below the for
loop.
The line after that sets the timeline running and specifies the refresh policy. In this case, the timeline will refresh after using up all the current entries.
Build and run on your device, sign in and let the list load. Then close the app, add your widget and watch it update every 5 seconds.
Widget updating entry every 5 seconds.
I could watch this all day :].
If you didn't scroll the list, the widget will run out of entries after 20 items. If you wait that long, you'll see it pause while it refreshes.
Note : This is beta software. If you're not getting the expected results, try deleting the app from your device and restarting your device. Also, remember that widgets aren't meant to run with time intervals measured in seconds. Very short intervals are just more convenient in a tutorial setting. But as a consequence, the waiting time for timeline refresh feels interminable! And a final warning: Don't leave the 5-second widget running on your device, as it will drain the battery.
Enabling User Customization
I picked 5 seconds for the timeline interval, so I wouldn't have to wait long to see the updates. If you want a shorter or longer interval, just change the value in the code. Or ... create an intent that will let you set the interval by editing the widget, right on your home screen!
Note : When you use the intent to change the interval, you won't see the effect until the widget refreshes its timeline.
Adding an Intent
First, add your intent: Create a new file ( Command-N ), search for "intent", select SiriKit Intent Definition File and name it TimelineInterval . Make sure its target is both emitron and EmitronWidgetExtension .
In the lower left corner of the intent's sidebar, click + and select New Intent .
Add new intent.
Name the intent TimelineInterval . Set up the Custom Intent as shown, with Category View :
Custom intent with category view.
And add a Parameter named interval of type Integer with default, minimum and maximum values as shown, and Type Field . Or set your own values and/or use a stepper.
Add interval parameter.
Reconfiguring Your Widget
In EmitronWidget.swift
, reconfigure your widget to IntentConfiguration
.
Change the Provider
protocol to IntentTimelineProvider
.
struct Provider: IntentTimelineProvider {
Change the definition of snapshot(with:completion:)
to:
public func snapshot( for configuration: TimelineIntervalIntent, with context: Context, completion: @escaping (Entry) -> Void ) {
Now, change the definition of timeline(with:completion:)
to:
public func timeline( for configuration: TimelineIntervalIntent, with context: Context, completion: @escaping (Timeline<Entry>) -> Void ) {
In timeline(for:with:completion)
, change interval
to use the configuration parameter:
let interval = configuration.interval as! Int
And finally, in EmitronWidget
, change StaticConfiguration(kind:provider:placeholder:)
to this:
IntentConfiguration( kind: kind, intent: TimelineIntervalIntent.self, provider: Provider(), placeholder: PlaceholderView() ) { entry in
Build and run on your device, sign in and let the list load. Close the app, add your widget, then long-press the widget. It flips over to show an Edit Widget button.
Edit widget button
Tap this button to change the interval value
Change the interval value.
Setting a new interval
Where To Go From Here?
Download the final project using the Download Materials button at the top or bottom of the tutorial.
This tutorial showed you how to leverage code from a large app to create a widget that displays items from the app's own repository. Here are a few ideas for you to add to your Emitron widget:
-
Design a view for the large size widget that displays two or more entries. Look at Apple's EmojiRangers sample app to see how to modify
EntryView
for widget families. -
Add a
widgetURL
toEntryView
so tapping the widget opens Emitron in that item's detail view. - Add intents to let users set the app's filters from the widget.
I hope you enjoyed this tutorial! Building widgets is fun!
If you have any comments or questions, feel free to join in the forum discussion below!
以上所述就是小编给大家介绍的《Getting Started With Widgets [FREE]》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
马云现象的经济学分析:互联网经济的八个关键命题
胡晓鹏 / 上海社会科学院出版社 / 2016-11-1 / CNY 68.00
互联网经济的产生、发展与扩张,在冲击传统经济理论观点的同时,也彰显了自身理论体系的独特内核,并与那种立足于工业经济时代的经典理论发生显著分野。今天看来,“马云”们的成功是中国经济长期“重制造、轻服务,重产能、轻消费,重国有、轻民营”发展逻辑的结果。但互联网经济的发展却不应仅仅止步于商业技巧的翻新,还需要在理论上进行一番审慎的思考。对此,我们不禁要问:互联网经济驱动交易发生的机理是什么?用户基数和诚......一起来看看 《马云现象的经济学分析:互联网经济的八个关键命题》 这本书的介绍吧!