6.8K Views
November 30, 23
スライド概要
ブエノスアイレス/アルゼンチンのiOSカンファレンス Swiftble 2023で発表したトークの資料です。(英語)
※資料には埋め込み動画が多いのですが、PDF上では再生出来ません。
I am a break dancer (b-boy)
Swiftable 2023 The Widget Revolution: Exploring New Possibilities with a TODO List app on a widget Osamu “Lil Ossa” Hiraoka, iOS engineer
Introduction My name is Osamu Hiraoka a.k.a Lil Ossa from Japan 🇯🇵 iOS engineer and Break-boy @littleossa
Agenda Why I should talk about The Widget Revolution
Agenda Why I should talk about The Widget Revolution The widget update from iOS 17 includes interactivity and animations
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
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
Before iOS 16
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)
}
}
}
Widget Item 1 "widget://complete?id=\(item.id)"
Widget Main App Item 1 .onOpenURL “widget://complete?id=\(item.id)"
import SwiftUI struct MainApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in print(url) // Print: widget://complete?id=1 } } } }
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() } } } }
What should be done in the end?
import UIKit UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
import UIKit extension UIControl { static func backToHomeScreenOfDevice() { UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) } }
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) } }) } }
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() } } } }
Before iOS 16
I will become The King of the Widget
Real King has come
My app is Dead
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
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
Learn more about interactive Widget Bring widgets to life WWDC23
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()
}
}
Button(intent: CompleteTodoItemIntent()) { Text("Complete") } Toggle("Complete", isOn: true, intent: CompleteTodoItemIntent())
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
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
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
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()
}
}
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) }
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()
}
}
struct DismissViewButton: View { var body: some View { Button(intent: DismissViewIntent()) { Image(systemName: "xmark") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }
struct ParentView: View { var body: some View { VStack { Text("Parent") PresentViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) } } } Parent
struct NextView: View { var body: some View { VStack { HStack { DismissViewButton() .frame(width: 44, height: 44) .padding() .foregroundStyle(.blue) Spacer() } Spacer() } .background(.white) } }
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() } } } } }
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() } } } } }
if viewIsPresented { Color.black.opacity(0.7) VStack { Spacer().frame(height: 64) NextView() } }
if viewIsPresented { Color.black.opacity(0.7) .transition(.opacity) VStack { Spacer().frame(height: 64) NextView() } }
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))) }
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))) }
struct DismissViewButton: View { var body: some View { Button(intent: DismissViewIntent()) { Image(systemName: "xmark") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }
struct DismissViewButton: View { var body: some View { Button(intent: DismissViewIntent()) { Image(systemName: "chevron.left") .resizable() } } } #Preview { DismissViewButton() .frame(width: 44, height: 44) .foregroundStyle(.blue) }
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() } } } }
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
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...")) } }
🤔
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()
}
}
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()
}
}
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() } } } }
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()
}
}
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()
}
}
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
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()
}
}
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() } }
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)
}
}
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() } }
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() } }
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
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 } }
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()
}
}
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()
}
}
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) } } } }
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
UIControl.backToHomeScreen()
🤔
How can I determine if the Widget is installed?
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) } } } } }
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() } } } }
• Transition for push and modal sheet • Create Item • Update Item • Delete Item • Show Error • Absolutely prevent the use of the main app
Question
Will Apple approve it? ?
YES
UltimateWidgetTodo https://apps.apple.com/us/app/ultimatewidgettodo/id6471950020
How was the journey into the widget revolution?
GitHub: UltimateWidgetTodo https://github.com/littleossa/UltimateWidgetTodo/
Open Source - You Can Do It
Have a good Widget life
Thanks