Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Agenda Why I should talk about The Widget Revolution

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Before iOS 16

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

"widget://complete?id=\(item.id)" 􀀀 *UFN Widget

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

import SwiftUI 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() import WidgetKit

Slide 13

Slide 13 text

What should be done in the end?

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Before iOS 16

Slide 19

Slide 19 text

I will become The King of the Widget

Slide 20

Slide 20 text

Real King has come 􀣺

Slide 21

Slide 21 text

My app is Dead

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Bring widgets to life WWDC23 Learn more about interactive Widget

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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() } } } } }

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

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() } }

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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() } } } }

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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...")) } }

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

🤔

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

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() } }

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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() } }

Slide 65

Slide 65 text

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() } }

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

struct : 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() } } DeleteItemIntent

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

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) } }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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 } }

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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) } } } }

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

UIControl.backToHomeScreen()

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

🤔

Slide 87

Slide 87 text

How can I determine if the Widget is installed?

Slide 88

Slide 88 text

import WidgetKit extension WidgetCenter { func 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) } } } } } getCurrentConfiguration()

Slide 89

Slide 89 text

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() }

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

Question

Slide 93

Slide 93 text

Will Apple approve it? 􀣺 ?

Slide 94

Slide 94 text

YES

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

How was the journey into the widget revolution?

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

Open Source - You Can Do It

Slide 99

Slide 99 text

Have a good Widget life

Slide 100

Slide 100 text

Thanks