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.🙇

Avatar for Lil Ossa リルオッサ

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