The Widget Revolution

6.3K Views

November 30, 23

スライド概要

ブエノスアイレス/アルゼンチンのiOSカンファレンス Swiftble 2023で発表したトークの資料です。(英語)

※資料には埋め込み動画が多いのですが、PDF上では再生出来ません。

profile-image

I am a break dancer (b-boy)

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Swiftable 2023 The Widget Revolution: Exploring New Possibilities with a TODO List app on a widget Osamu “Lil Ossa” Hiraoka, iOS engineer

2.

Introduction My name is Osamu Hiraoka a.k.a Lil Ossa from Japan 🇯🇵 iOS engineer and Break-boy @littleossa

3.

Agenda Why I should talk about The Widget Revolution

4.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations

5.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations Explore widget possibility with making Todo app on only a widget

6.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations Explore widget possibility with making Todo app on only a widget

7.

Before iOS 16

8.
[beta]
struct TodoListRow: View {
let item: TodoItem
var body: some View {
HStack {
Link(destination: URL(string: "widget://complete?id=\(item.id)"
)!) {
Image(systemName: "circle")
.resizable()
.frame(width: 44, height: 44)
.foregroundColor(.gray)
}
Text(item.name)
}
}
}

9.

􀀀 Widget Item 1 "widget://complete?id=\(item.id)"

10.

􀀀 Widget Main App Item 1 .onOpenURL “widget://complete?id=\(item.id)"

11.

import SwiftUI struct MainApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in print(url) // Print: widget://complete?id=1 } } } }

12.

import SwiftUI import WidgetKit struct MainApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in print(url) // Print: widget://complete?id=1 // Implementation of what you want to do WidgetCenter.shared.reloadAllTimelines() } } } }

13.

What should be done in the end?

14.

import UIKit UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)

15.

import UIKit extension UIControl { static func backToHomeScreenOfDevice() { UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) } }

16.

struct ContentView: View { var body: some View { Button(action: { UIControl.backToHomeScreenOfDevice() UIControl.backToHomeScreenOfDevice() }, label: { RoundedRectangle(cornerRadius: 8) .fill(.blue) .frame(width: 120, height: 48) .overlay { Text("Magic") .foregroundStyle(.white) } }) } }

17.

import SwiftUI import WidgetKit struct MainApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in // Implementation of what you want to do WidgetCenter.shared.reloadAllTimelines() UIControl.backToHomeScreenOfDevice() } } } }

18.

Before iOS 16

19.

I will become The King of the Widget

20.

􀣺 Real King has come

21.

My app is Dead

22.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations Explore widget possibility with making Todo app on only a widget

23.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations Explore widget possibility with making Todo app on only a widget

24.

Learn more about interactive Widget Bring widgets to life WWDC23

25.
[beta]
import AppIntents
struct CompleteTodoItemIntent
CompleteTodoItemIntent: AppIntent {
static var title: LocalizedStringResource = "Complete a Todo item"
func perform() async throws -> some IntentResult {
// Implementation what you want to do
return .result()
}
}

26.

Button(intent: CompleteTodoItemIntent()) { Text("Complete") } Toggle("Complete", isOn: true, intent: CompleteTodoItemIntent())

27.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations Explore widget possibility with making Todo app on only a widget

28.

Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations Explore widget possibility with making Todo app on only a widget

29.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

30.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

31.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

32.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

33.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

34.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

35.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

36.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

37.
[beta]
import AppIntents
struct PresentViewIntent: AppIntent {
static var title: LocalizedStringResource = "Present View”
func perform() async throws -> some IntentResult {
UserDefaults.standard.setValue(true, forKey: “view_is_presented")
return .result()
}
}

38.

􀁍 struct PresentViewButton: View { var body: some View { Button(intent: PresentViewIntent()) { Image(systemName: "plus.circle.fill") .resizable() } } } #Preview { PresentViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }

39.
[beta]
import AppIntents
struct DismissViewIntent: AppIntent {
static var title: LocalizedStringResource = "Dismiss View”
func perform() async throws -> some IntentResult {
UserDefaults.standard.setValue(false, forKey: “view_is_presented")
return .result()
}
}

40.

􀆄 struct DismissViewButton: View { var body: some View { Button(intent: DismissViewIntent()) { Image(systemName: "xmark") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }

41.

􀁍 struct ParentView: View { var body: some View { VStack { Text("Parent") PresentViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) } } } Parent

42.

􀆄 struct NextView: View { var body: some View { VStack { HStack { DismissViewButton() .frame(width: 44, height: 44) .padding() .foregroundStyle(.blue) Spacer() } Spacer() } .background(.white) } }

43.

struct ContentView: View { @AppStorage(“view_is_presented") var viewIsPresented = false var body: some View { ZStack { ParentView() if viewIsPresented { Color.black.opacity(0.7) VStack { Spacer().frame(height: 64) NextView() } } } } }

45.

struct ContentView: View { @AppStorage(“view_is_presented") var viewIsPresented = false var body: some View { ZStack { ParentView() if viewIsPresented { Color.black.opacity(0.7) VStack { Spacer().frame(height: 64) NextView() } } } } }

46.

if viewIsPresented { Color.black.opacity(0.7) VStack { Spacer().frame(height: 64) NextView() } }

47.

if viewIsPresented { Color.black.opacity(0.7) .transition(.opacity) VStack { Spacer().frame(height: 64) NextView() } }

48.

if viewIsPresented { Color.black.opacity(0.7) .transition(.opacity) VStack { Spacer().frame(height: 64) NextView() } .transition(.asymmetric(insertion: .push(from: .top), removal: .push(from: .top))) }

49.

if viewIsPresented { Color.black.opacity(0.7) .transition(.opacity) VStack { Spacer().frame(height: 64) NextView() .clipShape(.rect(cornerRadius: 12, style: .circular)) } .transition(.asymmetric(insertion: .push(from: .top), removal: .push(from: .top))) }

51.

􀆄 struct DismissViewButton: View { var body: some View { Button(intent: DismissViewIntent()) { Image(systemName: "xmark") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }

52.

􀆉 struct DismissViewButton: View { var body: some View { Button(intent: DismissViewIntent()) { Image(systemName: "chevron.left") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }

53.

struct ContentView: View { @AppStorage(“view_is_presented") var viewIsPresented = false var body: some View { if viewIsPresented { NextView() .transition(.asymmetric(insertion: .push(from: .leading), removal: .push(from: .leading))) } else { Color.yellow .overlay { ParentView() } } } }

55.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

56.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

57.

import SwiftUI struct ContentView: View { @AppStorage(“input_text") var inputText = "" var body: some View { TextField("Task name”, text: $inputText, prompt: Text("Input task name here...")) } }

59.

🤔

61.
[beta]
struct KeyboardLetterKeyIntent: AppIntent {
static var title: LocalizedStringResource = "Keyboard letter key"
@Parameter(title: "Keyboard letter key”) var letter: String
init() {}
init(letter: String) {
self.letter = letter
}
func perform() async throws -> some IntentResult {
return .result()
}
}

62.
[beta]
struct KeyboardLetterKeyIntent: AppIntent {
// ...

func perform() async throws -> some IntentResult {
let text = UserDefaults.standard.string(forKey: "input_text") ?? ""
let latestText = text + letter
UserDefaults.standard.set(latestText, forKey: "input_text")
return .result()
}
}

63.

struct InputFormView: View { @AppStorage("input_text") var inputText = "" var body: some View { RoundedRectangle(cornerRadius: 6) .stroke(lineWidth: 1) .frame(width: 296, height: 40) .overlay { ABCDEFGHIJK HStack { Text(inputText) .padding() Spacer() } } } }

64.
[beta]
struct AddItemIntent: AppIntent {
static var title: LocalizedStringResource = "Add item intent"
func perform() async throws -> some IntentResult {
let name = UserDefaults.standard.string(forKey: "input_text") ?? ""
TodoItemStore.shared.addItem(name)
return .result()
}
}

65.
[beta]
struct UpdateItemIntent: AppIntent {
static var title: LocalizedStringResource = "Update item intent"
@Parameter(title: "Item ID”) var id: String
init() {}
init(id: String) {
self.id = id
}
func perform() async throws -> some IntentResult {
let name = UserDefaults.standard.string(forKey: "input_text") ?? ""
TodoItemStore.shared.updateItem(id: id, toName: name)
return .result()
}
}

66.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

67.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

68.
[beta]
struct DeleteItemIntent : AppIntent {
static var title: LocalizedStringResource = "Delete item intent"
@Parameter(title: "Item ID”) var id: String
init() {}
init(id: String) {
self.id = id
}
func perform() async throws -> some IntentResult {
TodoItemStore.shared.deleteItem(id: id)
return .result()
}
}

69.

struct TodoItemRow: View { let item: TodoItem var body: some View { HStack { Toggle(isOn: false, intent: DeleteItemIntent(id: item.id)) { Image(systemName: "circle") .font(.largeTitle.weight(.light)) } Text(item.name) Spacer() } .padding() } }

71.
[beta]
struct DeleteToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
.font(.largeTitle.weight(.light))
.foregroundColor(configuration.isOn ? .blue : .gray)
}
}

72.

struct TodoItemRow: View { let item: TodoItem var body: some View { HStack { Toggle(isOn: false, intent:DeleteItemIntent DeleteItemIntent(id: item.id)) { Image(systemName: "circle") .font(.largeTitle.weight(.light)) } Text(item.name) Spacer() } .padding() } }

73.

struct TodoItemRow: View { let item: TodoItem var body: some View { HStack { Toggle(“Delete Item”, isOn: false, intent: DeleteItemIntent(id: item.id)) .toggleStyle(DeleteToggleStyle()) Text(item.name) Spacer() } .padding() } }

75.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

76.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

77.

enum WidgetError: String, Error { case emptyInputText var info: Info { switch self { case .emptyInputText: return .init(title: "Input Error”, message: "Empty tasks not allowed.”) } } struct Info { let title: LocalizedStringKey let message: LocalizedStringKey } }

78.
[beta]
struct AddItemIntent: AppIntent {
static var title: LocalizedStringResource = "Add item intent"
func perform() async throws -> some IntentResult {
let name = UserDefaults.standard.string(forKey: "input_text") ?? ""
TodoItemStore.shared.addItem(name)
return .result()
}
}

79.
[beta]
struct AddItemIntent: AppIntent {
static var title: LocalizedStringResource = "Add item intent"
func perform() async throws -> some IntentResult {
let name = UserDefaults.standard.string(forKey: "input_text") ?? ""
if name.isEmpty {
UserDefaults.standard.setValue(WidgetError.inputTextEmpty.rawValue,
forKey: "widget_error")
} else {
TodoItemStore.shared.addItem(name)
}
return .result()
}
}

80.

struct ContentView: View { @AppStorage("widget_error") var errorKey = "" var error: WidgetError? { return WidgetError(rawValue: errorKey) } var body: some View { ZStack { Button("Add Empty item", intent: AddItemIntent()) if let error { ErrorAlertView(error: error) .transition(.opacity) } } } }

82.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

83.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

84.

UIControl.backToHomeScreen()

86.

🤔

87.

How can I determine if the Widget is installed?

88.

import WidgetKit extension WidgetCenter { func getCurrentConfiguration() async throws -> [WidgetInfo] { try await withCheckedThrowingContinuation { continuation in self.getCurrentConfigurations { result in switch result { case .success(let info): continuation.resume(returning: info) case .failure(let error): continuation.resume(throwing: error) } } } } }

89.

struct SampleApp: App { @Environment(\.scenePhase) var scenePhase // ... .onChange(of: scenePhase) { _, newValue in Task { guard newValue == .active else { return } let info = try await WidgetCenter.shared.getCurrentConfiguration()) if !info.isEmpty { UIControl.backToHomeScreenOfDevice() } } } }

90.

• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app

92.

Question

93.

􀣺 Will Apple approve it? ?

94.

YES

95.

UltimateWidgetTodo https://apps.apple.com/us/app/ultimatewidgettodo/id6471950020

96.

How was the journey into the widget revolution?

97.

GitHub: UltimateWidgetTodo https://github.com/littleossa/UltimateWidgetTodo/

98.

Open Source - You Can Do It

99.

Have a good Widget life

100.

Thanks