Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Widget Revolution

The Widget Revolution

This is a presentation I talked at Swiftable 2023 in Buenos Aires, Argentina

With iOS 17, The possibilities of widgets have expanded infinitely, Thanks to AppIntents and animations.
I hope this talk has inspired you with new ideas.

I apologize for the inconvenience. The document contains several embedded videos, but they cannot be played in the PDF format.🙇

Lil Ossa リルオッサ

November 30, 2023
Tweet

More Decks by Lil Ossa リルオッサ

Other Decks in Programming

Transcript

  1. Osamu “Lil Ossa” Hiraoka, iOS engineer The Widget Revolution: Exploring

    New Possibilities with a TODO List app on a widget Swiftable 2023
  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 The

    widget update from iOS 17 includes interactivity and animations
  4. 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
  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. 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)"
  7. import SwiftUI struct MainApp: App { var body: some Scene

    { WindowGroup { ContentView() .onOpenURL { url in print(url) } } } } // Print: widget://complete?id=1
  8. 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
  9. 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()
  10. 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()
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. • Transition for push and modal sheet • Create Item

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

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

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

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

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

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

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

    • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
  24. 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
  25. 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 􀁍
  26. 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
  27. struct DismissViewButton: View { var body: some View { Button(intent:

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

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

    { HStack { DismissViewButton() .frame(width: 44, height: 44) .padding() .foregroundStyle(.blue) Spacer() } Spacer() } .background(.white) } } 􀆄
  30. 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() } } } } }
  31. 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() } }
  32. 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)))
  33. 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))
  34. struct DismissViewButton: View { var body: some View { Button(intent:

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

    DismissViewIntent()) { Image(systemName: "chevron.left") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) } 􀆉
  36. 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() } } } }
  37. • Transition for push and modal sheet • Create Item

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

    • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
  39. 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...")) } }
  40. 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() } }
  41. 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")
  42. 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
  43. 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() } }
  44. 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() } }
  45. • Transition for push and modal sheet • Create Item

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

    • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
  47. 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
  48. 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
  49. 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) } }
  50. 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)) }
  51. 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())
  52. • Transition for push and modal sheet • Create Item

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

    • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
  54. 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 } }
  55. 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)
  56. 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 { }
  57. 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) } } } }
  58. • Transition for push and modal sheet • Create Item

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

    • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
  60. 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()
  61. 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() }
  62. • Transition for push and modal sheet • Create Item

    • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
  63. YES