├── .gitignore
├── README.md
├── UltimateWidgetTodo.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── UltimateWidgetTodo.xcscheme
│ ├── UltimateWidgetTodoTests.xcscheme
│ └── UltimateWidgetTodoWidgetExtension.xcscheme
├── UltimateWidgetTodo
├── Extensions
│ ├── UIControl+Extensions.swift
│ └── WidgetCenter+Extensions.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── PrivacyInfo.xcprivacy
├── UltimateWidgetTodoApp.swift
└── Views
│ ├── OnboardingView.swift
│ └── ProhibitedView.swift
├── UltimateWidgetTodoTests
├── DataStoreTests
│ └── UserDefaultsStoreTests.swift
├── Extension
│ ├── SwiftDataStore+Extensions.swift
│ └── XCTestCase+Extensions.swift
├── ModelTests
│ ├── EmojiKeyboardContentTest.swift
│ └── ListControlTests.swift
├── RepositoryTests
│ ├── KeyboardInputRepositoryTests.swift
│ ├── ListDisplayRepositoryTests.swift
│ ├── ScreenStateRepositoryTests.swift
│ └── TodoItemRepositoryTests.swift
└── WidgetTodoCoreTests.swift
└── UltimateWidgetTodoWidget
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ ├── Contents.json
│ └── ultimate-widget-todo-icon.png
├── Contents.json
├── EmojiKeyboardBackground.colorset
│ └── Contents.json
├── KeyDarkGray.colorset
│ └── Contents.json
├── KeyShadow.colorset
│ └── Contents.json
├── KeyWhite.colorset
│ └── Contents.json
├── KeyboardBackground.colorset
│ └── Contents.json
├── StaticKeyWhite.colorset
│ └── Contents.json
└── WidgetBackground.colorset
│ └── Contents.json
├── DataStore
├── SwiftData
│ ├── SwiftDataStore.swift
│ └── TodoItem.swift
└── UserDefaultsStore
│ └── UserDefaultsStore.swift
├── Extensions
├── Color+Extensions.swift
└── LinerGradient+Extensions.swift
├── Info.plist
├── Model
├── EmojiKeyboardContent+Category.swift
├── EmojiKeyboardContent.swift
├── Enum
│ ├── EditTodoItemType.swift
│ ├── KeyboardInputMode.swift
│ ├── ScreenType.swift
│ └── WidgetError.swift
└── ListDisplayControl.swift
├── Repository
├── KeyboardInputRepository.swift
├── ListDisplayRepository.swift
├── ScreenStateRepository.swift
└── TodoItemRepository.swift
├── Views
├── EditTodoItem
│ ├── AddItemView.swift
│ ├── Components
│ │ ├── CloseButton.swift
│ │ └── InputForm.swift
│ └── EditItemView.swift
├── ErrorView
│ ├── Components
│ │ └── ErrorOKButton.swift
│ └── WidgetTodoErrorView.swift
├── Keyboard
│ ├── AlphabetKeyboard.swift
│ ├── EmojiKeyboard.swift
│ ├── Keys
│ │ ├── AlphabetModeKey.swift
│ │ ├── BackspaceKey.swift
│ │ ├── CapsLockKey.swift
│ │ ├── DoneKey.swift
│ │ ├── EmojiCategoryKey.swift
│ │ ├── EmojiContentMoveKey.swift
│ │ ├── EmojiModeKey.swift
│ │ ├── ExtraPunctuationMarksKey.swift
│ │ ├── KeyboardEmojiKey.swift
│ │ ├── KeyboardLetterKey.swift
│ │ ├── NumberModeKey.swift
│ │ └── SpaceKey.swift
│ ├── NumberAndPunctuationMarkKeyboard.swift
│ └── WidgetKeyboard.swift
├── Line.swift
└── Main
│ ├── MainView.swift
│ └── TodoItemList
│ ├── Components
│ ├── CompleteTodoItemButton.swift
│ ├── ListScrollButton.swift
│ ├── PlusButtonImage.swift
│ ├── PresentAddItemViewButton.swift
│ └── TodoItemListRow.swift
│ ├── TodoItemEmptyView.swift
│ └── TodoItemListView.swift
└── Widget
├── Contents
├── UltimateWidgetTodoWidget.swift
├── UltimateWidgetTodoWidgetBundle.swift
└── WidgetTodoView.swift
├── TodoItemEntry.swift
├── TodoTaskProvider.swift
├── WidgetBackground
└── WidgetBackgroundView.swift
├── WidgetConfig.swift
└── WidgetTodoCore.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UltimateWidgetTodo
2 |
3 |
4 |
5 | The **Ultimate TODO app** that allows adding, updating, and deleting tasks directly from the widget without launching the main app.
6 |
7 | **It enriches your widget life.**
8 |
9 | [](https://apps.apple.com/us/app/ultimatewidgettodo/id6471950020)
10 |
11 | 
12 |
13 | ## Demo
14 |
15 | ### Task Management
16 |
17 | Create|Update|Delete
18 | --|--|--
19 | ||
20 |
21 | ### Scroll
22 |
23 | Up|Down
24 | --|--
25 | |
26 |
27 | ### Transition
28 |
29 | Push-like|Modal-sheet-like
30 | --|--
31 | |
32 |
33 | ### Show alert
34 |
35 |
36 |
37 | ### Preventing the launch of the main app by forcibly returning to the home screen
38 |
39 |
40 |
41 | ## Installation
42 |
43 | It requires iOS 17 or later and Xcode 15 or later.
44 |
45 | ## Lisence
46 |
47 | This project is licensed under the MIT License.
48 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo.xcodeproj/xcshareddata/xcschemes/UltimateWidgetTodo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo.xcodeproj/xcshareddata/xcschemes/UltimateWidgetTodoTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
16 |
19 |
25 |
26 |
27 |
28 |
29 |
39 |
40 |
46 |
47 |
49 |
50 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo.xcodeproj/xcshareddata/xcschemes/UltimateWidgetTodoWidgetExtension.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
59 |
61 |
67 |
68 |
69 |
70 |
74 |
75 |
79 |
80 |
84 |
85 |
86 |
87 |
94 |
96 |
102 |
103 |
104 |
105 |
107 |
108 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/Extensions/UIControl+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIControl+Extensions.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import UIKit
8 |
9 | extension UIControl {
10 |
11 | static func backToHomeScreenOfDevice() {
12 | UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/Extensions/WidgetCenter+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetCenter+Extensions.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import WidgetKit
8 |
9 | extension WidgetCenter {
10 |
11 | func getInstalledWidgetInfo() async throws -> [WidgetInfo] {
12 | try await withCheckedThrowingContinuation { continuation in
13 | self.getCurrentConfigurations { result in
14 | switch result {
15 | case .success(let info):
16 | continuation.resume(returning: info)
17 | case .failure(let error):
18 | continuation.resume(throwing: error)
19 | }
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPITypeReasons
9 |
10 | 1C8F.1
11 |
12 | NSPrivacyAccessedAPIType
13 | NSPrivacyAccessedAPICategoryUserDefaults
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/UltimateWidgetTodoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UltimateWidgetTodoApp.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 | import WidgetKit
9 |
10 | @main
11 | struct UltimateWidgetTodoApp: App {
12 |
13 | @Environment(\.scenePhase) var scenePhase
14 | @State private var widgetInstallState: WidgetInstallState = .checking
15 |
16 | var body: some Scene {
17 | WindowGroup {
18 | switch widgetInstallState {
19 | case .checking:
20 | EmptyView()
21 | case .installed:
22 | ProhibitedView()
23 | case .error, .notInstall:
24 | OnboardingView()
25 | }
26 | }
27 | .onChange(of: scenePhase) { _, newValue in
28 | guard newValue == .active
29 | else { return }
30 |
31 | Task {
32 | await checkWidgetInstallState()
33 | }
34 | }
35 | }
36 |
37 | private func checkWidgetInstallState() async {
38 | widgetInstallState = .checking
39 |
40 | do {
41 | try await Task.sleep(for: .seconds(0.05))
42 | let info = try await WidgetCenter.shared.getInstalledWidgetInfo()
43 |
44 | if info.isEmpty {
45 | widgetInstallState = .notInstall
46 | return
47 | }
48 | widgetInstallState = .installed
49 | try await Task.sleep(for: .seconds(0.75))
50 | await UIControl.backToHomeScreenOfDevice()
51 | try await Task.sleep(for: .seconds(0.2))
52 | exit(0)
53 |
54 | } catch {
55 | widgetInstallState = .error
56 | }
57 | }
58 | }
59 |
60 | enum WidgetInstallState {
61 | case checking
62 | case error
63 | case installed
64 | case notInstall
65 | }
66 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/Views/OnboardingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingView.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct OnboardingView: View {
10 | var body: some View {
11 |
12 | VStack(spacing: 16) {
13 | Text("Ultimate\nWidgetTodo")
14 | .multilineTextAlignment(.center)
15 | .font(.largeTitle)
16 | .bold()
17 | .foregroundStyle(LinearGradient.ultimateBlue)
18 |
19 | Group {
20 | Text("The app specialized in managing Todo on the widget.")
21 |
22 | Text("To use this app, you need to add the widget to your home screen.")
23 | }
24 | .foregroundColor(Color(uiColor: .darkGray))
25 | .fontWeight(.semibold)
26 | .multilineTextAlignment(.center)
27 |
28 | Spacer()
29 | .frame(height: 16)
30 |
31 | VStack(spacing: 8) {
32 |
33 | Text("How to Add Widget")
34 | .font(.title)
35 | .bold()
36 |
37 | VStack(alignment: .leading, spacing: 4) {
38 | Text("1. Long press on an empty space on your home screen.")
39 |
40 | Text("2. Tap the + button that appears in the top.")
41 |
42 | Text("3. Select Add Widget when the widget selection screen appears.")
43 | Text("4. Place the widget anywhere you like on your home screen.")
44 | }
45 | .fixedSize(horizontal: false, vertical: true)
46 | }
47 |
48 | Spacer()
49 | .frame(height: 40)
50 |
51 | Button("Back to Home Screen") {
52 |
53 | UIControl.backToHomeScreenOfDevice()
54 | Task {
55 | try await Task.sleep(for: .seconds(0.2))
56 | exit(0)
57 | }
58 | }
59 | .foregroundColor(.white)
60 | .bold()
61 | .padding(24)
62 | .background {
63 | RoundedRectangle(cornerRadius: 36)
64 | .foregroundColor(.accentColor)
65 | }
66 | }
67 | .padding()
68 | }
69 | }
70 |
71 | #Preview {
72 | OnboardingView()
73 | }
74 |
--------------------------------------------------------------------------------
/UltimateWidgetTodo/Views/ProhibitedView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProhibitedView.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ProhibitedView: View {
10 |
11 | var body: some View {
12 |
13 | VStack(spacing: 16) {
14 |
15 | Text("Ultimate\nWidgetTodo")
16 | .font(.largeTitle)
17 | .bold()
18 | .foregroundStyle(LinearGradient.ultimateBlue)
19 |
20 | Text("Launching the main app is prohibited.")
21 | .font(.headline)
22 | .fontWeight(.semibold)
23 | }
24 | .multilineTextAlignment(.center)
25 | }
26 | }
27 |
28 | #Preview {
29 | ProhibitedView()
30 | }
31 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/DataStoreTests/UserDefaultsStoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsStoreTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class UserDefaultsStoreTests: XCTestCase {
11 |
12 | let store = UserDefaultsStore.testStore
13 |
14 | override func tearDownWithError() throws {
15 | store.removeAll()
16 | }
17 |
18 | func test_defaultValue() {
19 |
20 | store.emojiKeyboardIndex = 1
21 | store.error = .todoItemNameLimitExceeded
22 | store.frequentlyUsedEmojis = ["😊", "🚀"]
23 | store.inputText = "Input"
24 | store.isCapsLocked = true
25 | store.keyboardInputMode = .emoji
26 | store.listDisplayIndex = 10
27 | store.screenType = .addTodoItem
28 | XCTAssertEqual(store.emojiKeyboardIndex, 1)
29 | XCTAssertEqual(store.error, .todoItemNameLimitExceeded)
30 | XCTAssertEqual(store.frequentlyUsedEmojis, ["😊", "🚀"])
31 | XCTAssertEqual(store.inputText, "Input")
32 | XCTAssertTrue(store.isCapsLocked)
33 | XCTAssertEqual(store.keyboardInputMode, .emoji)
34 | XCTAssertEqual(store.listDisplayIndex, 10)
35 | XCTAssertEqual(store.screenType, .addTodoItem)
36 |
37 | store.removeAll()
38 | XCTAssertEqual(store.emojiKeyboardIndex, 0)
39 | XCTAssertNil(store.error)
40 | XCTAssertTrue(store.frequentlyUsedEmojis.isEmpty)
41 | XCTAssertEqual(store.inputText, "")
42 | XCTAssertFalse(store.isCapsLocked)
43 | XCTAssertEqual(store.keyboardInputMode, .alphabet)
44 | XCTAssertEqual(store.listDisplayIndex, 0)
45 | XCTAssertEqual(store.screenType, .main)
46 | }
47 |
48 | func test_emojiKeyboardIndex() {
49 | XCTAssertEqual(store.emojiKeyboardIndex, 0)
50 |
51 | store.emojiKeyboardIndex = 1
52 | XCTAssertEqual(store.emojiKeyboardIndex, 1)
53 | }
54 |
55 | func test_error() {
56 | XCTAssertNil(store.error)
57 |
58 | store.error = .todoItemDeletionFailure
59 | XCTAssertEqual(store.error, .todoItemDeletionFailure)
60 | store.error = .unknown
61 | XCTAssertEqual(store.error, .unknown)
62 | }
63 |
64 | func test_frequentlyUsedEmojis() {
65 | XCTAssertTrue(store.frequentlyUsedEmojis.isEmpty)
66 |
67 | store.frequentlyUsedEmojis = ["😊", "🚀", "🌟"]
68 | XCTAssertEqual(store.frequentlyUsedEmojis, ["😊", "🚀", "🌟"])
69 | }
70 |
71 | func test_inputText() {
72 |
73 | store.inputText = "Input"
74 | XCTAssertEqual(store.inputText, "Input")
75 | store.inputText = ""
76 | XCTAssertEqual(store.inputText, "")
77 | store.inputText = "😇"
78 | XCTAssertEqual(store.inputText, "😇")
79 | }
80 |
81 | func test_isCapsLocked() {
82 | store.isCapsLocked = true
83 | XCTAssertTrue(store.isCapsLocked)
84 | store.isCapsLocked = false
85 | XCTAssertFalse(store.isCapsLocked)
86 | }
87 |
88 | func test_keyboardInputMode() {
89 | store.keyboardInputMode = .emoji
90 | XCTAssertEqual(store.keyboardInputMode, .emoji)
91 | store.keyboardInputMode = .extraPunctuationMarks
92 | XCTAssertEqual(store.keyboardInputMode, .extraPunctuationMarks)
93 | store.keyboardInputMode = .number
94 | XCTAssertEqual(store.keyboardInputMode, .number)
95 | store.keyboardInputMode = .alphabet
96 | XCTAssertEqual(store.keyboardInputMode, .alphabet)
97 | }
98 |
99 | func test_listDisplayIndex() {
100 | XCTAssertEqual(store.listDisplayIndex, 0)
101 |
102 | store.listDisplayIndex = 2
103 | XCTAssertEqual(store.listDisplayIndex, 2)
104 | }
105 |
106 | func test_screenType() {
107 | store.screenType = .addTodoItem
108 | XCTAssertEqual(store.screenType, .addTodoItem)
109 | let uuid = UUID()
110 | store.screenType = .editTodoItem(id: uuid)
111 | XCTAssertEqual(store.screenType, .editTodoItem(id: uuid))
112 | store.screenType = .main
113 | XCTAssertEqual(store.screenType, .main)
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/Extension/SwiftDataStore+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDataStore+Extensions.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import SwiftData
8 | import XCTest
9 |
10 | @testable import UltimateWidgetTodo
11 |
12 | extension SwiftDataStore {
13 |
14 | @MainActor
15 | func fetchItem() -> [TodoItem] {
16 | let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.createDate, order: .reverse)])
17 | let items = try? self.context.fetch(descriptor)
18 | return items ?? []
19 | }
20 |
21 | @MainActor
22 | func clear() {
23 | try? self.context.delete(model: TodoItem.self)
24 | try? self.context.save()
25 | }
26 |
27 | func createItems(count: Int) -> [TodoItem] {
28 | var items: [TodoItem] = []
29 | for i in 0..(
12 | _ expression: @autoclosure () async throws -> T,
13 | _ message: @autoclosure () -> String = "",
14 | file: StaticString = #filePath,
15 | line: UInt = #line,
16 | _ errorHandler: (_ error: Error) -> Void = { _ in }
17 | ) async {
18 | do {
19 | _ = try await expression()
20 | // expected error to be thrown, but it was not
21 | let customMessage = message()
22 | if customMessage.isEmpty {
23 | XCTFail("Asynchronous call did not throw an error.", file: file, line: line)
24 | } else {
25 | XCTFail(customMessage, file: file, line: line)
26 | }
27 | } catch {
28 | errorHandler(error)
29 | }
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/ModelTests/EmojiKeyboardContentTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiKeyboardContentTest.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class EmojiKeyboardContentTest: XCTestCase {
11 |
12 | let content = EmojiKeyboardContent()
13 |
14 | func test_keyboardEndIndex() {
15 | XCTAssertEqual(content.keyboardEndIndex, EmojiKeyboardContent.Category.flags.info.endIndex)
16 | }
17 |
18 | func test_getCategory() {
19 | XCTAssertEqual(content.getCategory(for: 0), .frequentlyUsed)
20 | XCTAssertEqual(content.getCategory(for: 1), .smilyAndPeople)
21 | XCTAssertEqual(content.getCategory(for: 13), .smilyAndPeople)
22 | XCTAssertEqual(content.getCategory(for: 14), .animalsAndNature)
23 | XCTAssertEqual(content.getCategory(for: 19), .animalsAndNature)
24 | XCTAssertEqual(content.getCategory(for: 20), .foodAndDrinkStartIndex)
25 | XCTAssertEqual(content.getCategory(for: 23), .foodAndDrinkStartIndex)
26 | XCTAssertEqual(content.getCategory(for: 24), .activity)
27 | XCTAssertEqual(content.getCategory(for: 26), .activity)
28 | XCTAssertEqual(content.getCategory(for: 27), .travelAndPlaces)
29 | XCTAssertEqual(content.getCategory(for: 30), .travelAndPlaces)
30 | XCTAssertEqual(content.getCategory(for: 31), .objects)
31 | XCTAssertEqual(content.getCategory(for: 36), .objects)
32 | XCTAssertEqual(content.getCategory(for: 37), .symbols)
33 | XCTAssertEqual(content.getCategory(for: 44), .symbols)
34 | XCTAssertEqual(content.getCategory(for: 45), .flags)
35 | XCTAssertEqual(content.getCategory(for: 51), .flags)
36 | XCTAssertEqual(content.getCategory(for: 52), .frequentlyUsed)
37 |
38 | // Test default case
39 | XCTAssertEqual(content.getCategory(for: 100), .frequentlyUsed)
40 | }
41 |
42 | func test_categoryInfo() {
43 | XCTAssertEqual(EmojiKeyboardContent.Category.frequentlyUsed.info.icon, "🕐")
44 | XCTAssertEqual(EmojiKeyboardContent.Category.frequentlyUsed.info.title, "FREQUENTLY USED")
45 | XCTAssertEqual(EmojiKeyboardContent.Category.frequentlyUsed.info.startIndex, 0)
46 | XCTAssertEqual(EmojiKeyboardContent.Category.frequentlyUsed.info.endIndex, 0)
47 |
48 | XCTAssertEqual(EmojiKeyboardContent.Category.smilyAndPeople.info.icon, "😃")
49 | XCTAssertEqual(EmojiKeyboardContent.Category.smilyAndPeople.info.title, "SMILEYS & PEOPLE")
50 | XCTAssertEqual(EmojiKeyboardContent.Category.smilyAndPeople.info.startIndex, 1)
51 | XCTAssertEqual(EmojiKeyboardContent.Category.smilyAndPeople.info.endIndex, 13)
52 |
53 | XCTAssertEqual(EmojiKeyboardContent.Category.animalsAndNature.info.icon, "🐻❄️")
54 | XCTAssertEqual(EmojiKeyboardContent.Category.animalsAndNature.info.title, "ANIMALS & NATURE")
55 | XCTAssertEqual(EmojiKeyboardContent.Category.animalsAndNature.info.startIndex, 14)
56 | XCTAssertEqual(EmojiKeyboardContent.Category.animalsAndNature.info.endIndex, 19)
57 |
58 | XCTAssertEqual(EmojiKeyboardContent.Category.foodAndDrinkStartIndex.info.icon, "🍔")
59 | XCTAssertEqual(EmojiKeyboardContent.Category.foodAndDrinkStartIndex.info.title, "FOOD & DRINK")
60 | XCTAssertEqual(EmojiKeyboardContent.Category.foodAndDrinkStartIndex.info.startIndex, 20)
61 | XCTAssertEqual(EmojiKeyboardContent.Category.foodAndDrinkStartIndex.info.endIndex, 23)
62 |
63 | XCTAssertEqual(EmojiKeyboardContent.Category.activity.info.icon, "⚽️")
64 | XCTAssertEqual(EmojiKeyboardContent.Category.activity.info.title, "ACTIVITY")
65 | XCTAssertEqual(EmojiKeyboardContent.Category.activity.info.startIndex, 24)
66 | XCTAssertEqual(EmojiKeyboardContent.Category.activity.info.endIndex, 26)
67 |
68 | XCTAssertEqual(EmojiKeyboardContent.Category.travelAndPlaces.info.icon, "🚗")
69 | XCTAssertEqual(EmojiKeyboardContent.Category.travelAndPlaces.info.title, "TRAVEL & PLACES")
70 | XCTAssertEqual(EmojiKeyboardContent.Category.travelAndPlaces.info.startIndex, 27)
71 | XCTAssertEqual(EmojiKeyboardContent.Category.travelAndPlaces.info.endIndex, 30)
72 |
73 | XCTAssertEqual(EmojiKeyboardContent.Category.objects.info.icon, "💡")
74 | XCTAssertEqual(EmojiKeyboardContent.Category.objects.info.title, "OBJECTS")
75 | XCTAssertEqual(EmojiKeyboardContent.Category.objects.info.startIndex, 31)
76 | XCTAssertEqual(EmojiKeyboardContent.Category.objects.info.endIndex, 36)
77 |
78 | XCTAssertEqual(EmojiKeyboardContent.Category.symbols.info.icon, "🔣")
79 | XCTAssertEqual(EmojiKeyboardContent.Category.symbols.info.title, "SYMBOLS")
80 | XCTAssertEqual(EmojiKeyboardContent.Category.symbols.info.startIndex, 37)
81 | XCTAssertEqual(EmojiKeyboardContent.Category.symbols.info.endIndex, 44)
82 |
83 | XCTAssertEqual(EmojiKeyboardContent.Category.flags.info.icon, "🏁")
84 | XCTAssertEqual(EmojiKeyboardContent.Category.flags.info.title, "FLAGS")
85 | XCTAssertEqual(EmojiKeyboardContent.Category.flags.info.startIndex, 45)
86 | XCTAssertEqual(EmojiKeyboardContent.Category.flags.info.endIndex, 51)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/ModelTests/ListControlTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListControlTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class ListControlTests: XCTestCase {
11 |
12 | var testStore: SwiftDataStore = .testStore
13 |
14 | override func setUpWithError() throws {
15 | testStore = .testStore
16 | }
17 |
18 | func test_canAppearScrollButtons() {
19 |
20 | XCTContext.runActivity(named: "Items count is less than display limit") { _ in
21 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount - 1)
22 | let control = ListDisplayControl(currentIndex: 0, items: items)
23 | XCTAssertFalse(control.canAppearScrollButtons)
24 | }
25 |
26 | XCTContext.runActivity(named: "Items count is equal to display limit") { _ in
27 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount)
28 | let control = ListDisplayControl(currentIndex: 0, items: items)
29 | XCTAssertFalse(control.canAppearScrollButtons)
30 | }
31 |
32 | XCTContext.runActivity(named: "Items count is greater than display limit") { _ in
33 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount + 1)
34 | let control = ListDisplayControl(currentIndex: 0, items: items)
35 | XCTAssertTrue(control.canAppearScrollButtons)
36 | }
37 | }
38 |
39 | func test_displayItems() {
40 |
41 | let items = testStore.createItems(count: 10)
42 |
43 | XCTContext.runActivity(named: "Current index is 0") { _ in
44 | let control = ListDisplayControl(currentIndex: 0, items: items)
45 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
46 | XCTAssertEqual(control.displayItems[0].name, "0")
47 | XCTAssertEqual(control.displayItems[5].name, "5")
48 | }
49 |
50 | XCTContext.runActivity(named: "Current index is 3") { _ in
51 | let control = ListDisplayControl(currentIndex: 3, items: items)
52 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
53 | XCTAssertEqual(control.displayItems[0].name, "3")
54 | XCTAssertEqual(control.displayItems[5].name, "8")
55 | }
56 |
57 | XCTContext.runActivity(named: "Current index is 4") { _ in
58 | let control = ListDisplayControl(currentIndex: 4, items: items)
59 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
60 | XCTAssertEqual(control.displayItems[0].name, "4")
61 | XCTAssertEqual(control.displayItems[5].name, "9")
62 | }
63 |
64 | XCTContext.runActivity(named: "Current index is 5") { _ in
65 | let control = ListDisplayControl(currentIndex: 5, items: items)
66 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
67 | XCTAssertEqual(control.displayItems[0].name, "4")
68 | XCTAssertEqual(control.displayItems[5].name, "9")
69 | }
70 |
71 | XCTContext.runActivity(named: "Current index is 6") { _ in
72 | let control = ListDisplayControl(currentIndex: 6, items: items)
73 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
74 | XCTAssertEqual(control.displayItems[0].name, "4")
75 | XCTAssertEqual(control.displayItems[5].name, "9")
76 | }
77 |
78 | XCTContext.runActivity(named: "Current index is at the end") { _ in
79 | let control = ListDisplayControl(currentIndex: items.count - 1, items: items)
80 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
81 | XCTAssertEqual(control.displayItems[0].name, "4")
82 | XCTAssertEqual(control.displayItems[5].name, "9")
83 | }
84 | }
85 |
86 | func testIsDisableScrollUpButton() {
87 |
88 | XCTContext.runActivity(named: "Items count is less than display limit") { _ in
89 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount - 1)
90 | let control = ListDisplayControl(currentIndex: 0, items: items)
91 | XCTAssertTrue(control.isDisableScrollUpButton)
92 | }
93 |
94 | XCTContext.runActivity(named: "Items count is equal to display limit") { _ in
95 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount)
96 | let control = ListDisplayControl(currentIndex: 0, items: items)
97 | XCTAssertTrue(control.isDisableScrollUpButton)
98 | }
99 |
100 | XCTContext.runActivity(named: "Items count is greater than display limit and current index is 0") { _ in
101 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount + 1)
102 | let control = ListDisplayControl(currentIndex: 0, items: items)
103 | XCTAssertTrue(control.isDisableScrollUpButton)
104 | }
105 |
106 | XCTContext.runActivity(named: "Items count is greater than display limit and current index is 8") { _ in
107 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount + 8)
108 | let control = ListDisplayControl(currentIndex: 8, items: items)
109 | XCTAssertFalse(control.isDisableScrollUpButton)
110 | }
111 | }
112 |
113 | func test_isDisableScrollDownButton() {
114 |
115 | XCTContext.runActivity(named: "Items count is less than display limit") { _ in
116 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount - 1)
117 | let control = ListDisplayControl(currentIndex: 0, items: items)
118 | XCTAssertTrue(control.isDisableScrollDownButton)
119 | }
120 |
121 | XCTContext.runActivity(named: "Items count is equal to display limit") { _ in
122 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount)
123 | let control = ListDisplayControl(currentIndex: 0, items: items)
124 | XCTAssertTrue(control.isDisableScrollDownButton)
125 | }
126 |
127 | XCTContext.runActivity(named: "Items count is greater than display limit and current index is at the end") { _ in
128 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount + 1)
129 | let control = ListDisplayControl(currentIndex: items.count - 1, items: items)
130 | XCTAssertTrue(control.isDisableScrollDownButton)
131 | }
132 |
133 | XCTContext.runActivity(named: "Items count is greater than display limit and current index is not at the end") { _ in
134 | let items = testStore.createItems(count: WidgetConfig.displayTodoItemLimitCount + 20)
135 | let control = ListDisplayControl(currentIndex: 10, items: items)
136 | XCTAssertFalse(control.isDisableScrollDownButton)
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/RepositoryTests/KeyboardInputRepositoryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardInputRepositoryTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class KeyboardInputRepositoryTests: XCTestCase {
11 |
12 | let repository = KeyboardInputRepository(store: .testStore)
13 | let store: UserDefaultsStore = .testStore
14 | let emojiKeyboardContent = EmojiKeyboardContent()
15 |
16 | override func setUpWithError() throws {
17 | store.removeAll()
18 | }
19 |
20 | func test_currentEmojiCategory() {
21 | store.emojiKeyboardIndex = 0
22 | XCTAssertEqual(repository.currentEmojiCategory, .frequentlyUsed)
23 |
24 | store.emojiKeyboardIndex = 1
25 | XCTAssertEqual(repository.currentEmojiCategory, .smilyAndPeople)
26 |
27 | store.emojiKeyboardIndex = 14
28 | XCTAssertEqual(repository.currentEmojiCategory, .animalsAndNature)
29 |
30 | store.emojiKeyboardIndex = 20
31 | XCTAssertEqual(repository.currentEmojiCategory, .foodAndDrinkStartIndex)
32 |
33 | store.emojiKeyboardIndex = 24
34 | XCTAssertEqual(repository.currentEmojiCategory, .activity)
35 |
36 | store.emojiKeyboardIndex = 27
37 | XCTAssertEqual(repository.currentEmojiCategory, .travelAndPlaces)
38 |
39 | store.emojiKeyboardIndex = 31
40 | XCTAssertEqual(repository.currentEmojiCategory, .objects)
41 |
42 | store.emojiKeyboardIndex = 37
43 | XCTAssertEqual(repository.currentEmojiCategory, .symbols)
44 |
45 | store.emojiKeyboardIndex = 45
46 | XCTAssertEqual(repository.currentEmojiCategory, .flags)
47 |
48 | store.emojiKeyboardIndex = 52
49 | XCTAssertEqual(repository.currentEmojiCategory, .frequentlyUsed)
50 | }
51 |
52 | func test_currentEmojiContent() {
53 |
54 | XCTContext.runActivity(named: "emojiKeyboardIndex0 is frequentlyUsed") { _ in
55 | store.emojiKeyboardIndex = 0
56 | XCTAssertEqual(repository.currentEmojiContent, store.frequentlyUsedEmojis)
57 | }
58 |
59 | XCTContext.runActivity(named: "emojiKeyboardIndex is within the valid range") { _ in
60 | store.emojiKeyboardIndex = 15
61 | XCTAssertEqual(repository.currentEmojiContent, emojiKeyboardContent.getEmojis(for: 15))
62 | }
63 |
64 | XCTContext.runActivity(named: "emojiKeyboardIndex is greater than the keyboardEndIndex") { _ in
65 | store.emojiKeyboardIndex = emojiKeyboardContent.keyboardEndIndex + 1
66 | XCTAssertEqual(repository.currentEmojiContent, store.frequentlyUsedEmojis)
67 | }
68 | }
69 |
70 | func test_isEmojiLastContent() {
71 | store.emojiKeyboardIndex = 0
72 | XCTAssertFalse(repository.isEmojiLastContent)
73 |
74 | store.emojiKeyboardIndex = emojiKeyboardContent.keyboardEndIndex
75 | XCTAssertTrue(repository.isEmojiLastContent)
76 | }
77 |
78 | func test_appendFrequentlyUsedEmoji() {
79 | store.frequentlyUsedEmojis = ["😃", "🐱", "🍎"]
80 |
81 | XCTContext.runActivity(named: "Emoji is not in the list") { _ in
82 | repository.appendFrequentlyUsedEmoji("🚀")
83 | XCTAssertEqual(store.frequentlyUsedEmojis, ["🚀", "😃", "🐱", "🍎"])
84 | }
85 |
86 | XCTContext.runActivity(named: "Emoji is already in the list") { _ in
87 | repository.appendFrequentlyUsedEmoji("😃")
88 | XCTAssertEqual(store.frequentlyUsedEmojis, ["😃", "🚀", "🐱", "🍎"])
89 | }
90 |
91 |
92 | XCTContext.runActivity(named: "Emoji list is at the limit") { _ in
93 | store.frequentlyUsedEmojis = ["🧜", "🧜♂️", "🧚♀️", "🧚", "🧚♂️", "👼", "🤰", "🫄", "🫃", "🤱", "👩🍼", "🧑🍼", "👨🍼", "🙇♀️", "🙇", "🙇♂️", "💁♀️", "💁", "💁♂️", "🙅♀️", "🙅", "🙅♂️", "🙆♀️", "🙆", "🙆♂️", "🙋♀️", "🙋", "🙋♂️", "🧏♀️", "🧏", "🧏♂️", "🤦♀️", "🤦", "🤦🏻♂️", "🤷♀️", "🤷", "🤷♂️", "🙎♀️", "🙎", "🙎♂️"]
94 | XCTAssertEqual(store.frequentlyUsedEmojis.count, WidgetConfig.emojiKeyboardContentLimitCount)
95 | repository.appendFrequentlyUsedEmoji("🎉")
96 | XCTAssertEqual(store.frequentlyUsedEmojis, ["🎉", "🧜", "🧜♂️", "🧚♀️", "🧚", "🧚♂️", "👼", "🤰", "🫄", "🫃", "🤱", "👩🍼", "🧑🍼", "👨🍼", "🙇♀️", "🙇", "🙇♂️", "💁♀️", "💁", "💁♂️", "🙅♀️", "🙅", "🙅♂️", "🙆♀️", "🙆", "🙆♂️", "🙋♀️", "🙋", "🙋♂️", "🧏♀️", "🧏", "🧏♂️", "🤦♀️", "🤦", "🤦🏻♂️", "🤷♀️", "🤷", "🤷♂️", "🙎♀️", "🙎"])
97 | }
98 | }
99 |
100 | func test_clearInputText() {
101 | store.inputText = "Test"
102 | XCTAssertEqual(repository.inputText, "Test")
103 |
104 | repository.clearInputText()
105 | XCTAssertEqual(repository.inputText, "")
106 | }
107 |
108 | func test_input() {
109 | repository.input("A")
110 | XCTAssertEqual(repository.inputText, "A")
111 | repository.input("B")
112 | XCTAssertEqual(repository.inputText, "AB")
113 | }
114 |
115 | func test_changeMode() {
116 | repository.changeMode(into: .emoji)
117 | XCTAssertEqual(repository.inputMode, .emoji)
118 |
119 | repository.changeMode(into: .alphabet)
120 | XCTAssertEqual(repository.inputMode, .alphabet)
121 |
122 | repository.changeMode(into: .extraPunctuationMarks)
123 | XCTAssertEqual(repository.inputMode, .extraPunctuationMarks)
124 |
125 | repository.changeMode(into: .number)
126 | XCTAssertEqual(repository.inputMode, .number)
127 | }
128 |
129 | func test_deleteLastCharacter() {
130 | repository.input("Test")
131 | repository.deleteLastCharacter()
132 | XCTAssertEqual(repository.inputText, "Tes")
133 | }
134 |
135 | func test_goBackEmojiContent() {
136 | store.emojiKeyboardIndex = 3
137 | repository.goBackEmojiContent()
138 | XCTAssertEqual(repository.emojiKeyboardIndex, 2)
139 | repository.goBackEmojiContent()
140 | XCTAssertEqual(repository.emojiKeyboardIndex, 1)
141 | repository.goBackEmojiContent()
142 | XCTAssertEqual(repository.emojiKeyboardIndex, 0)
143 | repository.goBackEmojiContent()
144 | XCTAssertEqual(repository.emojiKeyboardIndex, 0)
145 | }
146 |
147 | func test_goForwardEmojiContent() {
148 | store.emojiKeyboardIndex = 49
149 | repository.goForwardEmojiContent()
150 | XCTAssertEqual(repository.emojiKeyboardIndex, 50)
151 | repository.goForwardEmojiContent()
152 | XCTAssertEqual(repository.emojiKeyboardIndex, 51)
153 | repository.goForwardEmojiContent()
154 | XCTAssertEqual(repository.emojiKeyboardIndex, 51)
155 | }
156 |
157 | func test_moveEmojiContent() {
158 | repository.moveEmojiContent(for: 2)
159 | XCTAssertEqual(repository.emojiKeyboardIndex, 2)
160 | }
161 |
162 | func testToggleCapsLock() {
163 | store.isCapsLocked = true
164 | XCTAssertTrue(repository.isCapsLocked)
165 | repository.toggleCapsLock()
166 | XCTAssertFalse(repository.isCapsLocked)
167 | repository.toggleCapsLock()
168 | XCTAssertTrue(repository.isCapsLocked)
169 |
170 | repository.toggleCapsLock(to: true)
171 | XCTAssertTrue(repository.isCapsLocked)
172 | repository.toggleCapsLock(to: false)
173 | XCTAssertFalse(repository.isCapsLocked)
174 | repository.toggleCapsLock(to: false)
175 | XCTAssertFalse(repository.isCapsLocked)
176 | repository.toggleCapsLock(to: true)
177 | XCTAssertTrue(repository.isCapsLocked)
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/RepositoryTests/ListDisplayRepositoryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListDisplayRepositoryTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class ListDisplayRepositoryTests: XCTestCase {
11 |
12 | let repository = ListDisplayRepository(store: .testStore)
13 | let store = UserDefaultsStore.testStore
14 |
15 | override func setUpWithError() throws {
16 | store.removeAll()
17 | }
18 |
19 | func test_scrollDownList() {
20 | let initialIndex = repository.currentIndex
21 | repository.scrollDownList()
22 | XCTAssertEqual(repository.currentIndex, initialIndex + 1)
23 | }
24 |
25 | func test_scrollUpList() {
26 | store.listDisplayIndex = 10
27 | let initialIndex = repository.currentIndex
28 | repository.scrollUpList()
29 | XCTAssertEqual(repository.currentIndex, initialIndex - 1)
30 | }
31 |
32 | func test_updateIndex() {
33 | repository.updateIndex(to: 5)
34 | XCTAssertEqual(repository.currentIndex, 5)
35 | repository.updateIndex(to: 10)
36 | XCTAssertEqual(repository.currentIndex, 10)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/RepositoryTests/ScreenStateRepositoryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenStateRepositoryTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class ScreenStateRepositoryTests: XCTestCase {
11 |
12 | let repository = ScreenStateRepository(store: .testStore)
13 | let store = UserDefaultsStore.testStore
14 |
15 | override func setUpWithError() throws {
16 | store.removeAll()
17 | }
18 |
19 | func test_currentScreen() {
20 | XCTAssertEqual(repository.currentScreen, .main)
21 | store.screenType = .addTodoItem
22 | XCTAssertEqual(repository.currentScreen, .addTodoItem)
23 | }
24 |
25 | func test_changeScreen() {
26 | repository.changeScreen(into: .addTodoItem)
27 | XCTAssertEqual(repository.currentScreen, .addTodoItem)
28 |
29 | repository.changeScreen(into: .main)
30 | XCTAssertEqual(repository.currentScreen, .main)
31 |
32 | let uuid = UUID()
33 | repository.changeScreen(into: .editTodoItem(id: uuid))
34 | XCTAssertEqual(repository.currentScreen, .editTodoItem(id: uuid))
35 | }
36 |
37 | func test_error() {
38 | let repository = ScreenStateRepository()
39 | XCTAssertNil(repository.error)
40 |
41 | let error = WidgetError.todoItemNameLimitExceeded
42 | repository.setError(error)
43 | XCTAssertEqual(repository.error, error)
44 |
45 | repository.resetError()
46 | XCTAssertNil(repository.error)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/RepositoryTests/TodoItemRepositoryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItemRepositoryTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import SwiftData
8 | import XCTest
9 | @testable import UltimateWidgetTodo
10 |
11 | final class TodoItemRepositoryTests: XCTestCase {
12 |
13 | @MainActor
14 | override func setUpWithError() throws {
15 | SwiftDataStore.testStore.clear()
16 | }
17 |
18 | @MainActor
19 | func test_addItem() {
20 | let repository = TodoItemRepository(store: .testStore)
21 | SwiftDataStore.testStore.clear()
22 |
23 | var items = SwiftDataStore.testStore.fetchItem()
24 | XCTAssertEqual(items.count, 0)
25 | let itemName = "Test Item"
26 | repository.addItem(name: itemName)
27 | items = SwiftDataStore.testStore.fetchItem()
28 | XCTAssertEqual(items.count, 1)
29 | XCTAssertEqual(items[0].name, itemName)
30 | let itemName2 = "Test Item2"
31 | repository.addItem(name: itemName2)
32 | items = SwiftDataStore.testStore.fetchItem()
33 | XCTAssertEqual(items.count, 2)
34 | XCTAssertEqual(items[0].name, itemName2)
35 | XCTAssertEqual(items[1].name, itemName)
36 | }
37 |
38 | @MainActor
39 | func test_deleteItem() {
40 | let repository = TodoItemRepository(store: .testStore)
41 | SwiftDataStore.testStore.clear()
42 |
43 | let itemName = "Item to be deleted"
44 | repository.addItem(name: itemName)
45 | var items = SwiftDataStore.testStore.fetchItem()
46 | XCTAssertEqual(items.count, 1)
47 | let id = items[0].itemId
48 |
49 | do {
50 | try repository.deleteItem(id: id)
51 | items = SwiftDataStore.testStore.fetchItem()
52 | XCTAssertEqual(items.count, 0)
53 | } catch {
54 | XCTFail("Failed to delete the item: \(error)")
55 | }
56 | }
57 |
58 | @MainActor
59 | func test_fetchItem() {
60 | let repository = TodoItemRepository(store: .testStore)
61 | SwiftDataStore.testStore.clear()
62 |
63 | let itemName = "Test Item"
64 | repository.addItem(name: itemName)
65 | let items = SwiftDataStore.testStore.fetchItem()
66 | XCTAssertEqual(items.count, 1)
67 | let id = items[0].itemId
68 |
69 | let itemName2 = "Test Item2"
70 | repository.addItem(name: itemName2)
71 | do {
72 | let fetchedItem = try repository.fetchItem(id: id)
73 | XCTAssertEqual(items[0].name, fetchedItem.name)
74 | } catch {
75 | XCTFail("Failed to fetch the item: \(error)")
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoTests/WidgetTodoCoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTodoCoreTests.swift
3 | // UltimateWidgetTodoTests
4 | //
5 | //
6 |
7 | import XCTest
8 | @testable import UltimateWidgetTodo
9 |
10 | final class WidgetTodoCoreTests: XCTestCase {
11 |
12 | let core = WidgetTodoCore.test
13 | let store = UserDefaultsStore.testStore
14 | var swiftDataStore = SwiftDataStore.testStore
15 |
16 | @MainActor
17 | override func setUpWithError() throws {
18 | store.removeAll()
19 | swiftDataStore.clear()
20 | }
21 |
22 | func test_currentEmojiCategory() {
23 |
24 | XCTAssertEqual(core.currentEmojiCategory, .frequentlyUsed)
25 | core.onTapEmojiCategoryKey(.smilyAndPeople)
26 | XCTAssertEqual(core.currentEmojiCategory, .smilyAndPeople)
27 | core.onTapEmojiCategoryKey(.animalsAndNature)
28 | XCTAssertEqual(core.currentEmojiCategory, .animalsAndNature)
29 | core.onTapEmojiCategoryKey(.flags)
30 | XCTAssertEqual(core.currentEmojiCategory, .flags)
31 | core.onTapEmojiCategoryKey(.frequentlyUsed)
32 | XCTAssertEqual(core.currentEmojiCategory, .frequentlyUsed)
33 | }
34 |
35 | func test_currentEmojiContent() {
36 |
37 | XCTContext.runActivity(named: "Frequently used emojis are selected") { _ in
38 | core.onTapEmojiKey("😃")
39 | core.onTapEmojiKey("😁")
40 | XCTAssertEqual(core.currentEmojiContent, ["😁", "😃"])
41 | }
42 |
43 | let animalsEmojiStartIndex = EmojiKeyboardContent.Category.animalsAndNature.info.startIndex
44 |
45 | XCTContext.runActivity(named: "When the category is switched to Animals and Nature") { _ in
46 | let animalEmojis = EmojiKeyboardContent().getEmojis(for: animalsEmojiStartIndex)
47 | core.onTapEmojiCategoryKey(.animalsAndNature)
48 | XCTAssertEqual(core.currentEmojiContent, animalEmojis)
49 | }
50 |
51 | XCTContext.runActivity(named: "When going back to Smily and People") { _ in
52 | let smilyEmojiLastIndex = animalsEmojiStartIndex - 1
53 | let smilyEmojis = EmojiKeyboardContent().getEmojis(for: smilyEmojiLastIndex)
54 | core.onTapGoBackEmojiContentKey()
55 | XCTAssertEqual(core.currentEmojiContent, smilyEmojis)
56 | }
57 |
58 | XCTContext.runActivity(named: "When going forward to Activity") { _ in
59 | let foodAndDrinkEndIndex = EmojiKeyboardContent.Category.foodAndDrinkStartIndex.info.endIndex
60 | let foodEmojis = EmojiKeyboardContent().getEmojis(for: foodAndDrinkEndIndex)
61 | core.onTapEmojiCategoryKey(.foodAndDrinkStartIndex)
62 | core.onTapGoForwardEmojiContentKey()
63 | core.onTapGoForwardEmojiContentKey()
64 | core.onTapGoForwardEmojiContentKey()
65 | XCTAssertEqual(core.currentEmojiContent, foodEmojis)
66 |
67 | let activityStartIndex = foodAndDrinkEndIndex + 1
68 | let activityEmojis = EmojiKeyboardContent().getEmojis(for: activityStartIndex)
69 |
70 | core.onTapGoForwardEmojiContentKey()
71 | XCTAssertEqual(core.currentEmojiContent, activityEmojis)
72 | }
73 | }
74 |
75 | func test_currentScreen() {
76 | XCTAssertEqual(core.currentScreen, .main)
77 |
78 | core.onTapPresentAddItemView()
79 | XCTAssertEqual(core.currentScreen, .addTodoItem)
80 | core.onTapCloseEditItemViewButton()
81 | XCTAssertEqual(core.currentScreen, .main)
82 | let itemId = UUID()
83 | core.onTapTodoItemListRow(id: itemId, name: "Test")
84 | XCTAssertEqual(core.currentScreen, .editTodoItem(id: itemId))
85 | core.onTapCloseEditItemViewButton()
86 | XCTAssertEqual(core.currentScreen, .main)
87 | }
88 |
89 | func test_error() {
90 | XCTAssertNil(core.error)
91 | core.showError(.todoItemNameLimitExceeded)
92 | XCTAssertEqual(core.error, .todoItemNameLimitExceeded)
93 | core.onTapErrorOKButton()
94 | XCTAssertNil(core.error)
95 | core.showError(.unknown)
96 | XCTAssertEqual(core.error, .unknown)
97 | }
98 |
99 | func test_inputText() {
100 | XCTAssertTrue(core.inputText.isEmpty)
101 | core.onTapCharacterKey("ABC")
102 | XCTAssertEqual(core.inputText, "ABC")
103 | core.onTapEmojiKey("😁")
104 | XCTAssertEqual(core.inputText, "ABC😁")
105 | }
106 |
107 | func test_isCapsLocked() {
108 | XCTAssertFalse(core.isCapsLocked)
109 | core.onTapCapsLockKey()
110 | XCTAssertTrue(core.isCapsLocked)
111 | core.onTapCapsLockKey()
112 | XCTAssertFalse(core.isCapsLocked)
113 | }
114 |
115 | func test_isEmojiFirstContent() {
116 | XCTAssertTrue(core.isEmojiFirstContent)
117 | core.onTapEmojiCategoryKey(.animalsAndNature)
118 | XCTAssertFalse(core.isEmojiFirstContent)
119 | }
120 |
121 | func test_isEmojiLastContent() {
122 | XCTAssertFalse(core.isEmojiLastContent)
123 | core.onTapEmojiCategoryKey(.flags)
124 | XCTAssertFalse(core.isEmojiLastContent)
125 |
126 | for _ in 0...6 {
127 | core.onTapGoForwardEmojiContentKey()
128 | }
129 | XCTAssertTrue(core.isEmojiLastContent)
130 | }
131 |
132 | func test_isEmojiMode() {
133 | XCTAssertFalse(core.isEmojiMode)
134 | core.onTapEmojiModeKey()
135 | XCTAssertTrue(core.isEmojiMode)
136 | }
137 |
138 | func test_isNumberMode() {
139 | XCTAssertFalse(core.isNumberMode)
140 | core.onTapNumberModeKey()
141 | XCTAssertTrue(core.isNumberMode)
142 | }
143 |
144 | func test_keyboardBottomRowKeys() {
145 | XCTAssertEqual(core.keyboardBottomRowKeys, KeyboardInputMode.alphabet.keyboardRow.bottomKeys)
146 | core.onTapNumberModeKey()
147 | XCTAssertEqual(core.keyboardBottomRowKeys, KeyboardInputMode.number.keyboardRow.bottomKeys)
148 | }
149 |
150 | func test_keyboardCenterRowKeys() {
151 | XCTAssertEqual(core.keyboardCenterRowKeys, KeyboardInputMode.alphabet.keyboardRow.centerKeys)
152 | core.onTapNumberModeKey()
153 | XCTAssertEqual(core.keyboardCenterRowKeys, KeyboardInputMode.number.keyboardRow.centerKeys)
154 | }
155 |
156 | func test_keyboardInputMode() {
157 | XCTAssertEqual(core.keyboardInputMode, .alphabet)
158 | core.onTapEmojiModeKey()
159 | XCTAssertEqual(core.keyboardInputMode, .emoji)
160 | core.onTapNumberModeKey()
161 | XCTAssertEqual(core.keyboardInputMode, .number)
162 | core.onTapExtraPunctuationMarksKey()
163 | XCTAssertEqual(core.keyboardInputMode, .extraPunctuationMarks)
164 | core.onTapAlphabetModeKey()
165 | XCTAssertEqual(core.keyboardInputMode, .alphabet)
166 | }
167 |
168 | func test_keyboardTopRowKeys() {
169 | XCTAssertEqual(core.keyboardTopRowKeys, KeyboardInputMode.alphabet.keyboardRow.topKeys)
170 | core.onTapNumberModeKey()
171 | XCTAssertEqual(core.keyboardTopRowKeys, KeyboardInputMode.number.keyboardRow.topKeys)
172 | }
173 |
174 | @MainActor
175 | func test_onTapAddItemDoneKey() async throws {
176 |
177 | // This text is empty
178 | XCTAssertTrue(core.inputText.isEmpty)
179 |
180 | core.onTapScrollDownList()
181 | XCTAssertEqual(store.listDisplayIndex, 1)
182 |
183 | var items = swiftDataStore.fetchItem()
184 | XCTAssertTrue(items.isEmpty)
185 |
186 | core.onTapPresentAddItemView()
187 | XCTAssertEqual(core.currentScreen, .addTodoItem)
188 |
189 | try await core.onTapAddItemDoneKey()
190 |
191 | XCTAssertTrue(core.inputText.isEmpty)
192 | XCTAssertEqual(store.listDisplayIndex, 1)
193 |
194 | items = swiftDataStore.fetchItem()
195 | XCTAssertTrue(items.isEmpty)
196 |
197 | XCTAssertEqual(core.currentScreen, .addTodoItem)
198 |
199 | // This text is over 26 characters - 28 count
200 | core.onTapCharacterKey("This text is over 26 letters")
201 |
202 | await XCTAssertThrowsAsyncError(try await core.onTapAddItemDoneKey()) { error in
203 | XCTAssertEqual(error as? WidgetError, .todoItemNameLimitExceeded)
204 | }
205 | XCTAssertEqual(core.inputText, "This text is over 26 letters")
206 | XCTAssertEqual(store.listDisplayIndex, 1)
207 |
208 | items = swiftDataStore.fetchItem()
209 | XCTAssertTrue(items.isEmpty)
210 |
211 | XCTAssertEqual(core.currentScreen, .addTodoItem)
212 |
213 | // This text is meeting the condition
214 | core.onTapBackspaceKey()
215 | core.onTapBackspaceKey()
216 | try await core.onTapAddItemDoneKey()
217 |
218 | XCTAssertTrue(core.inputText.isEmpty)
219 | XCTAssertEqual(store.listDisplayIndex, 0)
220 |
221 | items = swiftDataStore.fetchItem()
222 | XCTAssertEqual(items.count, 1)
223 |
224 | XCTAssertEqual(core.currentScreen, .main)
225 | }
226 |
227 | func test_onTapBackspaceKey() {
228 | core.onTapCharacterKey("ABCDEFG")
229 | XCTAssertEqual(core.inputText, "ABCDEFG")
230 | core.onTapBackspaceKey()
231 | XCTAssertEqual(core.inputText, "ABCDEF")
232 | core.onTapBackspaceKey()
233 | XCTAssertEqual(core.inputText, "ABCDE")
234 | }
235 |
236 | func test_onTapCloseEditItemViewButton() {
237 | core.onTapPresentAddItemView()
238 | XCTAssertEqual(core.currentScreen, .addTodoItem)
239 | core.onTapEmojiCategoryKey(.symbols)
240 | XCTAssertEqual(core.currentEmojiCategory, .symbols)
241 | core.onTapCharacterKey("Test")
242 | XCTAssertEqual(core.inputText, "Test")
243 | core.onTapEmojiModeKey()
244 | XCTAssertEqual(core.keyboardInputMode, .emoji)
245 |
246 | core.onTapCloseEditItemViewButton()
247 |
248 | XCTAssertEqual(core.currentScreen, .main)
249 | XCTAssertEqual(core.currentEmojiCategory, .frequentlyUsed)
250 | XCTAssertTrue(core.inputText.isEmpty)
251 | XCTAssertEqual(core.keyboardInputMode, .alphabet)
252 | }
253 |
254 | func test_onTapCompleteTodoItem() async throws {
255 |
256 | core.onTapCharacterKey("Test")
257 | try await core.onTapAddItemDoneKey()
258 |
259 | let item = await swiftDataStore.fetchItem().first
260 | XCTAssertNotNil(item)
261 | XCTAssertEqual(item?.name, "Test")
262 | let id = item?.itemId
263 |
264 | try await core.onTapCompleteTodoItem(id: id ?? UUID())
265 | let items = await swiftDataStore.fetchItem()
266 | XCTAssertTrue(items.isEmpty)
267 | }
268 |
269 | @MainActor
270 | func test_onTapEditItemDoneKey() async throws {
271 |
272 | core.onTapCharacterKey("Test")
273 | try await core.onTapAddItemDoneKey()
274 |
275 | var item = swiftDataStore.fetchItem().first
276 | XCTAssertNotNil(item)
277 | XCTAssertEqual(item?.name, "Test")
278 | let id = item?.itemId
279 | let updateDate = item?.updateDate
280 |
281 | // This text is empty
282 | XCTAssertTrue(core.inputText.isEmpty)
283 |
284 | core.onTapScrollDownList()
285 | XCTAssertEqual(store.listDisplayIndex, 1)
286 |
287 | core.onTapTodoItemListRow(id: id!, name: item!.name)
288 | XCTAssertEqual(core.currentScreen, .editTodoItem(id: id!))
289 |
290 | for _ in 0...3 {
291 | core.onTapBackspaceKey()
292 | }
293 | XCTAssertTrue(core.inputText.isEmpty)
294 |
295 | try await core.onTapEditItemDoneKey(id: id!)
296 |
297 | XCTAssertTrue(core.inputText.isEmpty)
298 | XCTAssertEqual(store.listDisplayIndex, 1)
299 |
300 | item = swiftDataStore.fetchItem().first
301 | XCTAssertNotNil(item)
302 | XCTAssertEqual(item?.name, "Test")
303 |
304 | XCTAssertEqual(core.currentScreen, .editTodoItem(id: id!))
305 |
306 | // This text is over 26 characters - 28 count
307 | core.onTapCharacterKey("This text is over 26 letters")
308 |
309 | await XCTAssertThrowsAsyncError(try await core.onTapEditItemDoneKey(id: id!)) { error in
310 | XCTAssertEqual(error as? WidgetError, .todoItemNameLimitExceeded)
311 | }
312 |
313 | XCTAssertEqual(core.inputText, "This text is over 26 letters")
314 | XCTAssertEqual(store.listDisplayIndex, 1)
315 |
316 | item = swiftDataStore.fetchItem().first
317 | XCTAssertNotNil(item)
318 | XCTAssertEqual(item?.name, "Test")
319 |
320 | XCTAssertEqual(core.currentScreen, .editTodoItem(id: id!))
321 |
322 | // This text is meeting the condition
323 | core.onTapBackspaceKey()
324 | core.onTapBackspaceKey()
325 | try await core.onTapEditItemDoneKey(id: id!)
326 |
327 | XCTAssertTrue(core.inputText.isEmpty)
328 | XCTAssertEqual(store.listDisplayIndex, 1)
329 |
330 | item = swiftDataStore.fetchItem().first
331 | XCTAssertNotNil(item)
332 | XCTAssertEqual(item?.name, "This text is over 26 lette")
333 | XCTAssertNotEqual(item?.updateDate, updateDate)
334 |
335 | XCTAssertEqual(core.currentScreen, .main)
336 | }
337 |
338 | func test_onTapPresentAddItemView() {
339 | XCTAssertFalse(core.isCapsLocked)
340 |
341 | XCTAssertEqual(store.screenType, .main)
342 | core.onTapPresentAddItemView()
343 | XCTAssertTrue(core.isCapsLocked)
344 | XCTAssertEqual(core.currentScreen, .addTodoItem)
345 | }
346 |
347 | func test_onTapScrollDownList() {
348 | let items = SwiftDataStore.testStore.createItems(count: 10)
349 |
350 | XCTContext.runActivity(named: "Current index is 0") { _ in
351 | let control = core.makeListDisplayControl(for: items)
352 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
353 | XCTAssertEqual(control.displayItems[0].name, "0")
354 | XCTAssertEqual(control.displayItems[5].name, "5")
355 | }
356 |
357 | core.onTapScrollDownList()
358 |
359 | XCTContext.runActivity(named: "Current index is 1") { _ in
360 | let control = core.makeListDisplayControl(for: items)
361 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
362 | XCTAssertEqual(control.displayItems[0].name, "1")
363 | XCTAssertEqual(control.displayItems[5].name, "6")
364 | }
365 |
366 | core.onTapScrollDownList()
367 |
368 | XCTContext.runActivity(named: "Current index is 2") { _ in
369 | let control = core.makeListDisplayControl(for: items)
370 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
371 | XCTAssertEqual(control.displayItems[0].name, "2")
372 | XCTAssertEqual(control.displayItems[5].name, "7")
373 | }
374 | }
375 |
376 | func test_onTapScrollUpList() {
377 | let items = SwiftDataStore.testStore.createItems(count: 10)
378 |
379 | store.listDisplayIndex = 5
380 | XCTContext.runActivity(named: "Current index is 5") { _ in
381 | let control = core.makeListDisplayControl(for: items)
382 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
383 | XCTAssertEqual(control.displayItems[0].name, "4")
384 | XCTAssertEqual(control.displayItems[5].name, "9")
385 | }
386 |
387 | core.onTapScrollUpList()
388 |
389 | XCTContext.runActivity(named: "Current index is 4") { _ in
390 | let control = core.makeListDisplayControl(for: items)
391 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
392 | XCTAssertEqual(control.displayItems[0].name, "4")
393 | XCTAssertEqual(control.displayItems[5].name, "9")
394 | }
395 |
396 | core.onTapScrollUpList()
397 |
398 | XCTContext.runActivity(named: "Current index is 3") { _ in
399 | let control = core.makeListDisplayControl(for: items)
400 | XCTAssertEqual(control.displayItems.count, WidgetConfig.displayTodoItemLimitCount)
401 | XCTAssertEqual(control.displayItems[0].name, "3")
402 | XCTAssertEqual(control.displayItems[5].name, "8")
403 | }
404 | }
405 |
406 | func test_onTapTodoItemListRow() {
407 |
408 | let uuid = UUID()
409 | XCTAssertTrue(core.inputText.isEmpty)
410 | XCTAssertEqual(core.currentScreen, .main)
411 | core.onTapCapsLockKey()
412 | XCTAssertTrue(core.inputText.isEmpty)
413 | core.onTapTodoItemListRow(id: uuid, name: "Test")
414 | XCTAssertEqual(core.currentScreen, .editTodoItem(id: uuid))
415 | XCTAssertEqual(core.inputText, "Test")
416 | XCTAssertFalse(core.isCapsLocked)
417 | }
418 |
419 | func test_showError() {
420 | XCTAssertNil(core.error)
421 | core.showError(.todoItemDeletionFailure)
422 | XCTAssertEqual(core.error, .todoItemDeletionFailure)
423 | core.showError(.todoItemDeletionFailure)
424 | XCTAssertEqual(core.error, .todoItemDeletionFailure)
425 | core.onTapErrorOKButton()
426 | XCTAssertNil(core.error)
427 | }
428 | }
429 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ultimate-widget-todo-icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/AppIcon.appiconset/ultimate-widget-todo-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littleossa/UltimateWidgetTodo/1cce1a08529712ee6150deda686668f43be63952/UltimateWidgetTodoWidget/Assets.xcassets/AppIcon.appiconset/ultimate-widget-todo-icon.png
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/EmojiKeyboardBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.943",
9 | "green" : "0.925",
10 | "red" : "0.916"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.168",
27 | "green" : "0.168",
28 | "red" : "0.168"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/KeyDarkGray.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.730",
9 | "green" : "0.694",
10 | "red" : "0.670"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.273",
27 | "green" : "0.273",
28 | "red" : "0.273"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/KeyShadow.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.642",
9 | "green" : "0.628",
10 | "red" : "0.628"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.134",
27 | "green" : "0.134",
28 | "red" : "0.134"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/KeyWhite.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.444",
27 | "green" : "0.444",
28 | "red" : "0.444"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/KeyboardBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.850",
9 | "green" : "0.831",
10 | "red" : "0.819"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.168",
27 | "green" : "0.168",
28 | "red" : "0.168"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/StaticKeyWhite.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.980",
9 | "green" : "0.980",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.160",
27 | "green" : "0.160",
28 | "red" : "0.159"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/DataStore/SwiftData/SwiftDataStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDataStore.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import Foundation
8 | import SwiftData
9 |
10 | class SwiftDataStore {
11 |
12 | private init(isTesting: Bool) {
13 | let schema = Schema([
14 | TodoItem.self,
15 | ])
16 | let configuration = ModelConfiguration(schema: schema,
17 | isStoredInMemoryOnly: isTesting)
18 |
19 | do {
20 | container = try ModelContainer(for: schema,
21 | configurations: [configuration])
22 | } catch {
23 | fatalError("Could not create ModelContainer: \(error)")
24 | }
25 | }
26 |
27 | static let shared = SwiftDataStore(isTesting: false)
28 | static let testStore = SwiftDataStore(isTesting: true)
29 |
30 | let container: ModelContainer
31 |
32 | @MainActor
33 | var context: ModelContext {
34 | return container.mainContext
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/DataStore/SwiftData/TodoItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItem.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 | import SwiftData
9 |
10 | @Model
11 | final class TodoItem {
12 | let itemId: UUID
13 | var name: String
14 | let createDate: Date
15 | var updateDate: Date
16 |
17 | init(name: String, createDate: Date) {
18 | self.itemId = UUID()
19 | self.name = name
20 | self.createDate = createDate
21 | self.updateDate = createDate
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/DataStore/UserDefaultsStore/UserDefaultsStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsStore.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | class UserDefaultsStore {
10 |
11 | static let shared = UserDefaultsStore()
12 | static let testStore = UserDefaultsStore(isTesting: true)
13 |
14 | private init(isTesting: Bool = false) {
15 | let suiteName = isTesting ? "testStore" : "widgetTodo"
16 | userDefaults = UserDefaults(suiteName: suiteName) ?? UserDefaults()
17 | }
18 |
19 | private let userDefaults: UserDefaults
20 |
21 | enum Key: String, CaseIterable {
22 | case editTodoItemId
23 | case emojiKeyboardIndex
24 | case error
25 | case frequentlyUsedEmojis
26 | case inputText
27 | case isCapsLocked
28 | case keyboardInputMode
29 | case listDisplayIndex
30 | case screenType
31 | }
32 |
33 | /// The ID required to navigate to the todo item editing screen
34 | private var editTodoItemId: UUID? {
35 | get {
36 | guard let value = userDefaults.string(forKey: Key.editTodoItemId.rawValue),
37 | let uuid = UUID(uuidString: value)
38 | else { return nil }
39 |
40 | return uuid
41 | }
42 | set {
43 | userDefaults.set(newValue?.uuidString, forKey: Key.editTodoItemId.rawValue)
44 | }
45 | }
46 |
47 | var emojiKeyboardIndex: Int {
48 | get {
49 | return userDefaults.integer(forKey: Key.emojiKeyboardIndex.rawValue)
50 | }
51 | set {
52 | userDefaults.set(newValue, forKey: Key.emojiKeyboardIndex.rawValue)
53 | }
54 | }
55 |
56 | var error: WidgetError? {
57 | get {
58 | let value = userDefaults.string(forKey: Key.error.rawValue) ?? ""
59 | return WidgetError(rawValue: value)
60 | }
61 | set {
62 | let value = newValue?.rawValue ?? ""
63 | userDefaults.set(value, forKey: Key.error.rawValue)
64 | }
65 | }
66 |
67 | var frequentlyUsedEmojis: [String] {
68 | get {
69 | let emojis = userDefaults.stringArray(forKey: Key.frequentlyUsedEmojis.rawValue)
70 | return emojis ?? []
71 | }
72 | set {
73 | userDefaults.set(newValue, forKey: Key.frequentlyUsedEmojis.rawValue)
74 | }
75 | }
76 |
77 | var inputText: String {
78 | get {
79 | return userDefaults.string(forKey: Key.inputText.rawValue) ?? ""
80 | }
81 | set {
82 | userDefaults.set(newValue, forKey: Key.inputText.rawValue)
83 | }
84 | }
85 |
86 | var isCapsLocked: Bool {
87 | get {
88 | return userDefaults.bool(forKey: Key.isCapsLocked.rawValue)
89 | }
90 | set {
91 | userDefaults.set(newValue, forKey: Key.isCapsLocked.rawValue)
92 | }
93 | }
94 |
95 | var keyboardInputMode: KeyboardInputMode {
96 | get {
97 | let value = userDefaults.string(forKey: Key.keyboardInputMode.rawValue) ?? ""
98 | if let mode = KeyboardInputMode(rawValue: value) {
99 | return mode
100 | }
101 | return .alphabet
102 | }
103 | set {
104 | userDefaults.set(newValue.rawValue, forKey: Key.keyboardInputMode.rawValue)
105 | }
106 | }
107 |
108 | var listDisplayIndex: Int {
109 | get {
110 | return userDefaults.integer(forKey: Key.listDisplayIndex.rawValue)
111 | }
112 | set {
113 | userDefaults.set(newValue, forKey: Key.listDisplayIndex.rawValue)
114 | }
115 | }
116 |
117 | var screenType: ScreenType {
118 | get {
119 | let value = userDefaults.string(forKey: Key.screenType.rawValue) ?? ""
120 |
121 | switch value {
122 | case ScreenType.main.screenName:
123 | return .main
124 | case ScreenType.addTodoItem.screenName:
125 | return .addTodoItem
126 | case ScreenType.editTodoItem(id: UUID()).screenName:
127 |
128 | if let editTodoItemId {
129 | return .editTodoItem(id: editTodoItemId)
130 | } else {
131 | fatalError("The screen name cannot be retrieved due to the lack of an editing ID")
132 | }
133 |
134 | default:
135 | print("This screen name is not defined")
136 | return .main
137 | }
138 | }
139 | set {
140 | switch newValue {
141 | case .main, .addTodoItem:
142 | break
143 | case .editTodoItem(let id):
144 | editTodoItemId = id
145 | }
146 | userDefaults.set(newValue.screenName, forKey: Key.screenType.rawValue)
147 | }
148 | }
149 |
150 | func removeAll() {
151 | Key.allCases.forEach {
152 | userDefaults.removeObject(forKey: $0.rawValue)
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Extensions/Color+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+Extensions.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | extension Color {
10 | static let label = Color(uiColor: .label)
11 | static let placeholderGray = Color(uiColor: .placeholderText)
12 | }
13 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Extensions/LinerGradient+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinearGradient+Extensions.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | extension LinearGradient {
10 | static let ultimateBlue = LinearGradient(colors: [.indigo, .blue, .cyan, .mint], startPoint: .topLeading, endPoint: .bottomTrailing)
11 | }
12 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.widgetkit-extension
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/EmojiKeyboardContent+Category.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiKeyboardContent+Category.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | extension EmojiKeyboardContent {
10 |
11 | enum Category: String, CaseIterable, Identifiable {
12 | case frequentlyUsed
13 | case smilyAndPeople
14 | case animalsAndNature
15 | case foodAndDrinkStartIndex
16 | case activity
17 | case travelAndPlaces
18 | case objects
19 | case symbols
20 | case flags
21 |
22 | struct Info {
23 | let icon: String
24 | let title: LocalizedStringKey
25 | let startIndex: Int
26 | let endIndex: Int
27 | }
28 |
29 | var id: String {
30 | return rawValue
31 | }
32 |
33 | var info: Info {
34 | switch self {
35 | case .frequentlyUsed:
36 | return .init(icon: "🕐",
37 | title: "FREQUENTLY USED",
38 | startIndex: 0,
39 | endIndex: 0)
40 | case .smilyAndPeople:
41 | return .init(icon: "😃",
42 | title: "SMILEYS & PEOPLE",
43 | startIndex: 1,
44 | endIndex: 13)
45 | case .animalsAndNature:
46 | return .init(icon: "🐻❄️",
47 | title: "ANIMALS & NATURE",
48 | startIndex: 14,
49 | endIndex: 19)
50 | case .foodAndDrinkStartIndex:
51 | return .init(icon: "🍔",
52 | title: "FOOD & DRINK",
53 | startIndex: 20,
54 | endIndex: 23)
55 | case .activity:
56 | return .init(icon: "⚽️",
57 | title: "ACTIVITY",
58 | startIndex: 24,
59 | endIndex: 26)
60 | case .travelAndPlaces:
61 | return .init(icon: "🚗",
62 | title: "TRAVEL & PLACES",
63 | startIndex: 27, endIndex: 30)
64 | case .objects:
65 | return .init(icon: "💡",
66 | title: "OBJECTS",
67 | startIndex: 31, endIndex: 36)
68 |
69 | case .symbols:
70 | return .init(icon: "🔣",
71 | title: "SYMBOLS",
72 | startIndex: 37, endIndex: 44)
73 | case .flags:
74 | return .init(icon: "🏁",
75 | title: "FLAGS",
76 | startIndex: 45,
77 | endIndex: 51)
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/EmojiKeyboardContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiKeyboardContent.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | struct EmojiKeyboardContent {
10 |
11 | let keyboardStartIndex = 0
12 | var keyboardEndIndex: Int {
13 | return Category.flags.info.endIndex
14 | }
15 |
16 | func getCategory(for index: Int) -> Category {
17 | switch index {
18 | case 0:
19 | return .frequentlyUsed
20 | case 1...13:
21 | return .smilyAndPeople
22 | case 14...19:
23 | return .animalsAndNature
24 | case 20...23:
25 | return .foodAndDrinkStartIndex
26 | case 24...26:
27 | return .activity
28 | case 27...30:
29 | return .travelAndPlaces
30 | case 31...36:
31 | return .objects
32 | case 37...44:
33 | return .symbols
34 | case 45...51:
35 | return .flags
36 |
37 | default:
38 | return .frequentlyUsed
39 | }
40 | }
41 |
42 | func getEmojis(for index: Int) -> [String] {
43 |
44 | switch index {
45 | // MARK: - Smily and people 1 ~ 13
46 | case 1:
47 | return ["😀", "😃", "😄", "😁", "😆", "🥹", "😅", "😂", "🤣", "🥲", "☺️", "😊", "😇", "🙂", "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", "😋", "😛", "😝", "😜", "🤪", "🤨", "🧐", "🤓", "😎", "🥸", "🤩", "🥳", "😏", "😒", "😞", "😔", "😟"]
48 | case 2:
49 | return ["😕", "🙁", "☹️", "😣", "😖", "😫", "😩", "🥺", "😢", "😭", "😤", "😠", "😡", "🤬", "🤯", "😳", "🥵", "🥶", "😶🌫️", "😱", "😨", "😰", "😥", "😓", "🤗", "🤔", "🫣", "🤭", "🫢", "🫡", "🤫", "🫠", "🤥", "😶", "🫥", "😐", "🫤", "😑", "🫨", "😬"]
50 | case 3:
51 | return ["🙄", "😯", "😦", "😧", "😮", "😲", "🥱", "😴", "🤤", "😪", "😮💨", "😵", "😵💫", "🤐", "🥴", "🤢", "🤮", "🤧", "😷", "🤒", "🤕", "🤑", "🤠", "😈", "👿", "👹", "👺", "🤡", "💩", "👻", "💀", "☠️", "👽", "👾", "🤖", "🎃", "😺", "😸", "😹", "😻"]
52 | case 4:
53 | return ["😼", "😽", "🙀", "😿", "😾", "🫶", "🤲", "👐", "🙌", "👏", "🤝", "👍", "👎", "👊", "✊", "🤛", "🤜", "🫷", "🫸", "🤞", "✌️", "🫰", "🤟", "🤘", "👌", "🤌", "🤏", "🫳", "🫴", "👈", "👉", "👆", "👇", "☝️", "✋", "🤚", "🖐️", "🖖", "👋", "🤙"]
54 | case 5:
55 | return ["🫲", "🫱", "💪", "🦾", "🖕", "✍️", "🙏", "🫵", "🦶", "🦵", "🦿", "🦿", "💄", "💋", "👄", "🫦", "🦷", "👅", "👂", "🦻", "👃", "👣", "👁️", "👀", "🫀", "🫁", "🧠", "🗣️", "👤", "👥", "🫂", "👶", "👧", "🧒", "👦", "👩", "🧑", "👨", "👩🦱", "🧑🦱"]
56 | case 6:
57 | return ["👨🦱", "👩🦰", "🧑🦰", "👨🦰", "👱♀️", "👱", "👱♂️", "👩🦳", "🧑🦳", "👨🦳", "👩🦲", "🧑🦲", "👨🦲", "🧔♀️", "🧔", "🧔♂️", "👵", "🧓", "👴", "👲", "👳♀️", "👳", "👳♂️", "🧕", "👮♀️", "👮", "👮♂️", "👷♀️", "👷", "👷♂️", "💂♀️", "💂", "💂♂️", "🕵️♀️", "🕵️", "🕵️♂️", "👩⚕️", "🧑⚕️", "👨⚕️", "👩🌾"]
58 | case 7:
59 | return ["🧑🌾", "👨🌾", "👩🍳", "🧑🍳", "👨🍳", "👩🎓", "🧑🎓", "👨🎓", "👩🎤", "🧑🎤", "👨🎤", "👩🏫", "🧑🏫", "👨🏫", "👩🏭", "🧑🏭", "👨🏭", "👩💻", "🧑💻", "👨💻", "👩💼", "🧑💼", "👨💼", "👩🔧", "🧑🔧", "👨🔧", "👩🔬", "🧑🔬", "👨🔬", "👩🎨", "🧑🎨", "👨🎨", "👩🚒", "🧑🚒", "👨🚒", "👩✈️", "🧑✈️", "👨✈️", "👩🚀", "🧑🚀"]
60 | case 8:
61 | return ["👨🚀", "👩⚖️", "🧑⚖️", "👨⚖️", "👰♀️", "👰", "👰♂️", "🤵♀️", "🤵", "🤵♂️", "👸", "🫅", "🤴", "🥷", "🦸♀️", "🦸", "🦸♂️", "🦹♀️", "🦹", "🦹♂️", "🤶", "🧑🎄", "🎅", "🧙♀️", "🧙", "🧙♂️", "🧝♀️", "🧝", "🧝♂️", "🧌", "🧛♀️", "🧛", "🧛♂️", "🧟♀️", "🧟", "🧟♂️", "🧞♀️", "🧞", "🧞♂️", "🧜♀️"]
62 | case 9:
63 | return ["🧜", "🧜♂️", "🧚♀️", "🧚", "🧚♂️", "👼", "🤰", "🫄", "🫃", "🤱", "👩🍼", "🧑🍼", "👨🍼", "🙇♀️", "🙇", "🙇♂️", "💁♀️", "💁", "💁♂️", "🙅♀️", "🙅", "🙅♂️", "🙆♀️", "🙆", "🙆♂️", "🙋♀️", "🙋", "🙋♂️", "🧏♀️", "🧏", "🧏♂️", "🤦♀️", "🤦", "🤦🏻♂️", "🤷♀️", "🤷", "🤷♂️", "🙎♀️", "🙎", "🙎♂️"]
64 | case 10:
65 | return ["🙍♀️", "🙍", "🙍♂️", "💇♀️", "💇", "💇♂️", "💆♀️", "💆", "💆♂️", "🧖♀️", "🧖", "🧖♂️", "💅", "🤳", "💃", "🕺", "👯♀️", "👯", "👯♂️", "🕴️", "👩🦽", "🧑🦽", "👨🦽", "👩🦼", "🧑🦼", "👨🦼", "🚶♀️", "🚶", "🚶♂️", "👩🦯", "🧑🦯", "👨🦯", "🧎♀️", "🧎", "🧎♂️", "🏃♀️", "🏃", "🏃♂️", "🧍♀️", "🧍"]
66 | case 11:
67 | return ["🧍♂️", "👫", "👭", "👬", "👩❤️👨", "👩❤️👩", "💑", "👨❤️👨", "👩❤️💋👨", "👩❤️💋👩", "💏", "👨❤️💋👨", "👨👩👦", "👨👩👧", "👨👩👧👦", "👨👩👦👦", "👨👩👧👧", "👩👩👦", "👩👩👧", "👩👩👧👦", "👩👩👦👦", "👩👩👧👧", "👨👨👦", "👨👨👧", "👨👨👧👦", "👨👨👦👦", "👨👨👧👧", "👩👦", "👩👧", "👩👧👦", "👩👦👦", "👩👧👧", "👨👦", "👨👧", "👨👧👦", "👨👦👦", "👨👧👧", "🪢", "🧶", "🧵"]
68 | case 12:
69 | return ["🪡", "🧥", "🥼", "🦺", "👚", "👕", "👖", "🩲", "🩳", "👔", "👗", "👙", "🩱", "👘", "🥻", "🩴", "🥿", "👠", "👡", "👢", "👞", "👟", "🥾", "🧦", "🧤", "🧣", "🎩", "🧢", "👒", "🎓", "⛑️", "🪖", "👑", "💍", "👝", "👛", "👜", "💼", "🎒", "🧳"]
70 | case 13:
71 | return ["👓", "🕶️", "🥽", "🌂"]
72 |
73 | // MARK: - Animal and Nature 14 ~ 19
74 | case 14:
75 | return ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐻❄️", "🐨", "🐯", "🦁", "🐮", "🐷", "🐽", "🐸", "🐵", "🙈", "🙉", "🙊", "🐒", "🐔", "🐧", "🐦", "🐤", "🐣", "🐥", "🪿", "🦆", "🐦⬛", "🦅", "🦉", "🦇", "🐺", "🐗", "🐴", "🦄", "🫎", "🐝", "🪱"]
76 | case 15:
77 | return ["🐛", "🦋", "🐌", "🐞", "🐜", "🪰", "🪲", "🪳", "🦟", "🦗", "🕷️", "🕸️", "🦂", "🐢", "🐍", "🦎", "🦖", "🦕", "🐙", "🦑", "🪼", "🦐", "🦞", "🦀", "🐡", "🐠", "🐟", "🐬", "🐳", "🐋", "🦈", "🦭", "🐊", "🐅", "🐆", "🦓", "🦍", "🦧", "🦣", "🐘"]
78 | case 16:
79 | return ["🦛", "🦏", "🐪", "🐫", "🦒", "🦘", "🦬", "🐃", "🐂", "🐄", "🫏", "🐎", "🐖", "🐏", "🐑", "🦙", "🐐", "🦌", "🐕", "🐩", "🦮", "🐕🦺", "🐈", "🐈⬛", "🪶", "🪽", "🐓", "🦃", "🦤", "🦚", "🦜", "🦢", "🦩", "🕊️", "🐇", "🦝", "🦨", "🦡", "🦫", "🦦"]
80 | case 17:
81 | return ["🦥", "🐁", "🐀", "🐿️", "🦔", "🐾", "🐉", "🐲", "🌵", "🎄", "🌲", "🌳", "🌴", "🪵", "🌱", "🌿", "☘️", "🍀", "🎍", "🪴", "🎋", "🍃", "🍂", "🍁", "🪺", "🪹", "🍄", "🐚", "🪸", "🪨", "🌾", "💐", "🌷", "🌹", "🥀", "🪻", "🪷", "🌺", "🌸", "🌼"]
82 | case 18:
83 | return ["🌻", "🌞", "🌝", "🌛", "🌜", "🌚", "🌕", "🌖", "🌗", "🌘", "🌑", "🌒", "🌓", "🌔", "🌙", "🌎", "🌍", "🌏", "🪐", "💫", "⭐️", "🌟", "✨", "⚡️", "☄️", "💥", "🔥", "🌪️", "🌈", "☀️", "🌤️", "⛅️", "🌥️", "☁️", "🌦️", "🌧️", "⛈️", "🌩️", "🌨️", "❄️"]
84 | case 19:
85 | return ["☃️", "⛄️", "🌬️", "💨", "💧", "💦", "🫧", "☔️", "☂️", "🌊", "🌫️"]
86 | // MARK: - Food and drink 20 ~ 23
87 | case 20:
88 | return ["🍏", "🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭", "🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🫛", "🥦", "🥬", "🥒", "🌶️", "🫑", "🌽", "🥕", "🫒", "🧄", "🧅", "🥔", "🍠", "🫚", "🥐", "🥯", "🍞", "🥖", "🥨", "🧀"]
89 | case 21:
90 | return ["🥚", "🍳", "🧈", "🥞", "🧇", "🥓", "🥩", "🍗", "🍖", "🦴", "🌭", "🍔", "🍟", "🍕", "🫓", "🥪", "🥙", "🧆", "🌮", "🌯", "🫔", "🥗", "🥘", "🫕", "🥫", "🫙", "🍝", "🍜", "🍲", "🍛", "🍣", "🍱", "🥟", "🦪", "🍤", "🍙", "🍚", "🍘", "🍥", "🥠"]
91 | case 22:
92 | return ["🥮", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🧁", "🍰", "🎂", "🍮", "🍭", "🍬", "🍫", "🍿", "🍩", "🍪", "🌰", "🥜", "🫘", "🍯", "🥛", "🫗", "🍼", "🫖", "☕️", "🍵", "🧃", "🥤", "🧋", "🍶", "🍺", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹", "🧉", "🍾"]
93 | case 23:
94 | return ["🧊", "🥄", "🍴", "🍽️", "🥣", "🥡", "🥢", "🧂"]
95 | // MARK: - Activity 24 ~ 26
96 | case 24:
97 | return ["⚽️", "🏀", "🏈", "⚾️", "🥎", "🎾", "🏐", "🏉", "🥏", "🎱", "🪀", "🏓", "🏸", "🏒", "🏑", "🥍", "🏏", "🪃", "🥅", "⛳️", "🪁", "🛝", "🏹", "🎣", "🤿", "🥊", "🥋", "🎽", "🛹", "🛼", "🛷", "⛸️", "🥌", "🎿", "⛷️", "🏂", "🪂", "🏋️♀️", "🏋️", "🏋️♂️"]
98 | case 25:
99 | return ["🤼♀️", "🤼", "🤼♂️", "🤸♀️", "🤸", "🤸♂️", "⛹️♀️", "⛹️", "⛹️♂️", "⛹️♂️", "🏌️♀️", "🏌️", "🏌️♂️", "🏇", "🧘♀️", "🧘", "🧘♂️", "🏄♀️", "🏄", "🏄♂️", "🏊♀️", "🏊", "🏊♂️", "🤽♀️", "🤽", "🤽♂️", "🚣♀️", "🚣", "🚣♂️", "🧗♀️", "🧗", "🧗♂️", "🚵♀️", "🚵", "🚵♂️", "🚴♀️", "🚴", "🚴🏻♂️", "🏆", "🥇"]
100 | case 26:
101 | return ["🥈", "🥉", "🏅", "🎖️", "🏵️", "🎗️", "🎫", "🎟️", "🎪", "🤹♀️", "🤹", "🤹♂️", "🎭", "🩰", "🎨", "🎬", "🎤", "🎧", "🎼", "🎹", "🪇", "🥁", "🪘", "🎷", "🎺", "🪗", "🎸", "🪕", "🎻", "🪈", "🎲", "♟️", "🎯", "🎳", "🎮", "🎰", "🧩"]
102 | // MARK: - Travel and Places 27 ~ 30
103 | case 27:
104 | return ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🦯", "🦽", "🦼", "🩼", "🛴", "🚲", "🛵", "🏍️", "🛺", "🛞", "🚨", "🚔", "🚍", "🚘", "🚖", "🚡", "🚠", "🚟", "🚃", "🚋", "🚞", "🚝", "🚄", "🚅", "🚈", "🚂"]
105 | case 28:
106 | return ["🚆", "🚇", "🚊", "🚉", "✈️", "🛫", "🛬", "🛩️", "💺", "🛰️", "🚀", "🛸", "🚁", "🛶", "⛵️", "🚤", "🛥️", "🛳️", "⛴️", "🚢", "🛟", "⚓️", "🪝", "⛽️", "🚧", "🚦", "🚥", "🚏", "🗺️", "🗿", "🗽", "🗼", "🏰", "🏯", "🏟️", "🎡", "🎢", "🎠", "⛲️", "⛱️"]
107 | case 29:
108 | return ["🏖️", "🏝️", "🏜️", "🌋", "⛰️", "🏔️", "🗻", "🏕️", "⛺️", "🛖", "🏠", "🏡", "🏘️", "🏚️", "🏗️", "🏭", "🏢", "🏬", "🏣", "🏤", "🏥", "🏦", "🏨", "🏪", "🏫", "🏩", "💒", "🏛️", "⛪️", "🕌", "🕍", "🛕", "🕋", "⛩️", "🛤️", "🛣️", "🗾", "🎑", "🏞️", "🌅"]
109 | case 30:
110 | return ["🌄", "🌠", "🎇", "🎆", "🌇", "🌆", "🏙️", "🌃", "🌌", "🌉", "🌁"]
111 | // MARK: - Objects 31 ~ 36
112 | case 31:
113 | return ["⌚️", "📱", "📲", "💻", "⌨️", "🖥️", "🖨️", "🖱️", "🖲️", "🕹️", "🗜️", "💽", "💾", "💿", "📀", "📼", "📷", "📸", "📹", "🎥", "📽️", "🎞️", "📞", "☎️", "📟", "📠", "📺", "📻", "🎙️", "🎚️", "🎛️", "🧭", "⏱️", "⏲️", "⏰", "🕰️", "⌛️", "⏳", "📡", "🔋"]
114 | case 32:
115 | return ["🪫", "🔌", "💡", "🔦", "🕯️", "🪔", "🧯", "🛢️", "💸", "💵", "💴", "💶", "💷", "🪙", "💰", "💳", "🪪", "💎", "⚖️", "🪜", "🧰", "🪛", "🔧", "🔨", "⚒️", "🛠️", "⛏️", "🪚", "🔩", "⚙️", "🪤", "🧱", "⛓️", "🧲", "🔫", "💣", "🧨", "🪓", "🔪", "🗡️"]
116 | case 33:
117 | return ["⚔️", "🛡️", "🚬", "⚰️", "🪦", "⚱️", "🏺", "🔮", "📿", "🧿", "🪬", "💈", "⚗️", "🔭", "🔬", "🕳️", "🩻", "🩹", "🩺", "💊", "💉", "🩸", "🧬", "🦠", "🧫", "🧪", "🌡️", "🧹", "🪠", "🧺", "🧻", "🚽", "🚰", "🚿", "🛁", "🛀", "🧼", "🪥", "🪒", "🪮"]
118 | case 34:
119 | return ["🧽", "🪣", "🧴", "🛎️", "🔑", "🗝️", "🚪", "🪑", "🛋️", "🛏️", "🛌", "🧸", "🪆", "🖼️", "🪞", "🪟", "🛍️", "🛒", "🎁", "🎈", "🎏", "🎀", "🪄", "🪅", "🎊", "🎉", "🎎", "🪭", "🏮", "🎐", "🪩", "🧧", "✉️", "📩", "📨", "📧", "💌", "📥", "📤", "📦"]
120 | case 35:
121 | return ["🏷️", "🪧", "📪", "📫", "📬", "📭", "📮", "📯", "📜", "📃", "📄", "📑", "🧾", "📊", "📈", "📉", "🗒️", "🗓️", "📆", "📅", "🗑️", "📇", "🗃️", "🗳️", "🗄️", "📋", "📁", "📂", "🗂️", "🗞️", "📰", "📓", "📔", "📒", "📕", "📗", "📘", "📙", "📚", "📖"]
122 | case 36:
123 | return ["🔖", "🧷", "🔗", "📎", "🖇️", "📐", "📏", "🧮", "📌", "📍", "✂️", "🖊️", "🖋️", "✒️", "🖌️", "🖍️", "📝", "✏️", "🔍", "🔎", "🔏", "🔐", "🔒", "🔓"]
124 | // MARK: - Symbols 37 ~ 44
125 | case 37:
126 | return ["🩷", "❤️", "🧡", "💛", "💚", "🩵", "💙", "💜", "🖤", "🩶", "🤍", "🤎", "💔", "❤️🔥", "❤️🩹", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟", "☮️", "✝️", "☪️", "🕉️", "☸️", "🪯", "✡️", "🔯", "🕎", "☯️", "☦️", "🛐", "⛎", "♈️", "♉️", "♊️"]
127 | case 38:
128 | return ["♋️", "♌️", "♍️", "♎️", "♏️", "♐️", "♑️", "♒️", "♓️", "🆔", "⚛️", "🉑", "☢️", "☣️", "📴", "📳", "🈶", "🈚️", "🈸", "🈺", "🈷️", "✴️", "🆚", "💮", "🉐", "㊙️", "㊗️", "🈴", "🈵", "🈹", "🈲", "🅰️", "🅱️", "🆎", "🆑", "🅾️", "🆘", "❌", "⭕️", "🛑"]
129 | case 39:
130 | return ["⛔️", "📛", "🚫", "💯", "💢", "♨️", "🚷", "🚯", "🚳", "🚱", "🔞", "📵", "🚭", "❗️", "❕", "❓", "❔", "‼️", "⁉️", "🔅", "🔆", "〽️", "⚠️", "🚸", "🔱", "⚜️", "🔰", "♻️", "✅", "🈯️", "💹", "❇️", "✳️", "❎", "🌐", "💠", "Ⓜ️", "🌀", "💤", "🏧"]
131 | case 40:
132 | return ["🚾", "♿️", "🅿️", "🛗", "🈳", "🈂️", "🛂", "🛃", "🛄", "🛅", "🛜", "🚹", "🚺", "🚼", "⚧️", "🚻", "🚮", "🎦", "📶", "🈁", "🔣", "ℹ️", "🔤", "🔡", "🔠", "🆖", "🆗", "🆙", "🆒", "🆕", "🆓", "0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣"]
133 | case 41:
134 | return ["9️⃣", "🔟", "🔢", "#️⃣", "*️⃣", "⏏️", "▶️", "⏸️", "⏯️", "⏹️", "⏺️", "⏭️", "⏮️", "⏩️", "⏪️", "⏫️", "⏬️", "◀️", "🔼", "➡️", "⬅️", "⬆️", "⬇️", "↗️", "↘️", "↙️", "↖️", "↕️", "↔️", "↪️", "↩️", "⤴️", "⤵️", "🔀", "🔁", "🔂", "🔄", "🔃", "🎵", "🎶"]
135 | case 42:
136 | return ["➕", "➖", "➗", "✖️", "🟰", "♾️", "💲", "💱", "™️", "©️", "®️", "👁️🗨️", "🔚", "🔙", "🔛", "🔝", "🔜", "〰️", "➰", "➿", "✔️", "☑️", "🔘", "🔴", "🟠", "🟡", "🟢", "🔵", "🟣", "⚫️", "⚪️", "🟤", "🔺", "🔻", "🔸", "🔹", "🔶", "🔷", "🔳", "🔲"]
137 | case 43:
138 | return ["▪️", "▫️", "◾️", "◽️", "◼️", "◻️", "🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "⬛️", "⬜️", "🟫", "🔈", "🔇", "🔉", "🔊", "🔔", "🔕", "📣", "📢", "💬", "💭", "🗯️", "♠️", "♣️", "♥️", "♦️", "🃏", "🎴", "🀄️", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖"]
139 | case 44:
140 | return ["🕗", "🕘", "🕙", "🕚", "🕛", "🕜", "🕝", "🕞", "🕟", "🕠", "🕡", "🕢", "🕣", "🕤", "🕥", "🕦", "🕧"]
141 | // MARK: - Flags 45 ~ 51
142 | case 45:
143 | return ["🏳️", "🏴", "🏴☠️", "🏁", "🚩", "🏳️🌈", "🏳️⚧️", "🇺🇳", "🇮🇸", "🇮🇪", "🇦🇿", "🇦🇫", "🇺🇸", "🇦🇪", "🇩🇿", "🇦🇷", "🇦🇼", "🇦🇱", "🇦🇲", "🇦🇮", "🇦🇴", "🇦🇬", "🇦🇩", "🇾🇪", "🇬🇧", "🏴", "🏴", "🏴", "🇮🇱", "🇮🇹", "🇮🇶", "🇮🇷", "🇮🇳", "🇮🇩", "🇼🇫", "🇺🇬", "🇺🇦", "🇺🇿", "🇺🇾", "🇪🇨"]
144 | case 46:
145 | return ["🇪🇬", "🇪🇪", "🇸🇿", "🇪🇹", "🇪🇷", "🇸🇻", "🇦🇺", "🇦🇹", "🇦🇽", "🇴🇲", "🇳🇱", "🇧🇶", "🇬🇭", "🇨🇻", "🇬🇬", "🇬🇾", "🇰🇿", "🇶🇦", "🇨🇦", "🇮🇨", "🇬🇦", "🇨🇲", "🇬🇲", "🇰🇭", "🇬🇳", "🇬🇼", "🇨🇾", "🇨🇺", "🇨🇼", "🇬🇷", "🇰🇮", "🇰🇬", "🇬🇹", "🇬🇵", "🇬🇺", "🇰🇼", "🇨🇰", "🇬🇱", "🇨🇽", "🇬🇩"]
146 |
147 | case 47:
148 | return ["🇭🇷", "🇰🇾", "🇰🇪", "🇨🇮", "🇨🇨", "🇨🇷", "🇽🇰", "🇰🇲", "🇨🇴", "🇨🇬", "🇨🇩", "🇸🇦", "🇬🇸", "🇼🇸", "🇧🇱", "🇸🇹", "🇿🇲", "🇵🇲", "🇸🇲", "🇸🇱", "🇩🇯", "🇬🇮", "🇯🇪", "🇯🇲", "🇬🇪", "🇸🇾", "🇸🇬", "🇸🇽", "🇿🇼", "🇨🇭", "🇸🇪", "🇸🇩", "🇪🇸", "🇸🇷", "🇱🇰", "🇸🇰", "🇸🇮", "🇸🇨", "🇸🇳", "🇷🇸"]
149 | case 48:
150 | return ["🇰🇳", "🇻🇨", "🇸🇭", "🇱🇨", "🇸🇴", "🇸🇧", "🇹🇨", "🇹🇭", "🇹🇯", "🇹🇿", "🇨🇿", "🇮🇴", "🇹🇩", "🇹🇳", "🇨🇱", "🇹🇻", "🇩🇰", "🇩🇪", "🇹🇬", "🇹🇰", "🇩🇴", "🇩🇲", "🇹🇹", "🇹🇲", "🇹🇷", "🇹🇴", "🇳🇬", "🇳🇷", "🇳🇦", "🇳🇺", "🇳🇮", "🇳🇪", "🇳🇨", "🇳🇿", "🇳🇵", "🇳🇫", "🇳🇴", "🇧🇭", "🇭🇹", "🇵🇰"]
151 | case 49:
152 | return ["🇻🇦", "🇵🇦", "🇻🇺", "🇧🇸", "🇵🇬", "🇧🇲", "🇵🇼", "🇵🇾", "🇧🇧", "🇵🇸", "🇭🇺", "🇧🇩", "🇵🇳", "🇫🇯", "🇯🇵", "🇫🇮", "🇧🇹", "🇵🇷", "🇫🇴", "🇫🇰", "🇧🇷", "🇫🇷", "🇧🇬", "🇧🇫", "🇧🇳", "🇧🇮", "🇻🇳", "🇧🇯", "🇻🇪", "🇧🇾", "🇧🇿", "🇵🇪", "🇧🇪", "🇵🇱", "🇧🇦", "🇧🇼", "🇧🇴", "🇵🇹", "🇭🇳", "🇲🇭"]
153 | case 50:
154 | return ["🇲🇴", "🇲🇬", "🇾🇹", "🇲🇼", "🇲🇱", "🇲🇹", "🇲🇶", "🇲🇾", "🇮🇲", "🇫🇲", "🇲🇲", "🇲🇽", "🇲🇺", "🇲🇷", "🇲🇿", "🇲🇨", "🇲🇻", "🇲🇩", "🇲🇦", "🇲🇳", "🇲🇪", "🇲🇸", "🇯🇴", "🇱🇦", "🇱🇻", "🇱🇹", "🇱🇾", "🇱🇮", "🇱🇷", "🇷🇴", "🇱🇺", "🇷🇼", "🇱🇸", "🇱🇧", "🇷🇪", "🇷🇺", "🇻🇬", "🇪🇺", "🇰🇷", "🇭🇰"]
155 | case 51:
156 | return ["🇪🇭", "🇬🇶", "🇹🇼", "🇨🇫", "🇨🇳", "🇹🇱", "🇿🇦", "🇸🇸", "🇦🇶", "🇯🇵", "🎌", "🇬🇫", "🇵🇫", "🇹🇫", "🇻🇮", "🇦🇸", "🇲🇰", "🇲🇵", "🇰🇵"]
157 |
158 | default:
159 | return .init([])
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/Enum/EditTodoItemType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditTodoItemType.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | enum EditTodoItemType {
10 | case addNewTodoItem
11 | case editTodoItem(id: UUID)
12 |
13 | var displayLabel: LocalizedStringKey {
14 | switch self {
15 | case .addNewTodoItem:
16 | return "New"
17 | case .editTodoItem:
18 | return "Edit"
19 | }
20 | }
21 |
22 | var closeButtonImageName: String {
23 | switch self {
24 | case .addNewTodoItem:
25 | return "xmark"
26 | case .editTodoItem:
27 | return "chevron.left"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/Enum/KeyboardInputMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardInputMode.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | enum KeyboardInputMode: String {
8 | case alphabet
9 | case emoji
10 | case extraPunctuationMarks
11 | case number
12 |
13 | var keyboardRow: KeyboardRow {
14 | switch self {
15 | case .alphabet:
16 | return .init(
17 | topKeys: ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
18 | centerKeys: ["A", "S", "D", "F", "G", "H", "J", "K", "L"],
19 | bottomKeys: ["Z", "X", "C", "V", "B", "N", "M"]
20 | )
21 | case .emoji:
22 | return .init(
23 | topKeys: [],
24 | centerKeys: [],
25 | bottomKeys: []
26 | )
27 | case .extraPunctuationMarks:
28 | return KeyboardRow(
29 | topKeys: ["[", "]", "{", "}", "#" , "%", "^", "*", "+", "="],
30 | centerKeys: ["_", "\\", "|", "~", "<" , ">", "€", "£", "¥", "•"],
31 | bottomKeys: [".", ",", "?", "!", "‘"]
32 | )
33 | case .number:
34 | return .init(
35 | topKeys: ["1", "2", "3", "4", "5" , "6", "7", "8", "9", "0"],
36 | centerKeys: ["-", "/", ":", ";", "(" , ")", "$", "&", "@", "“"],
37 | bottomKeys: [".", ",", "?", "!", "‘"]
38 | )
39 | }
40 | }
41 |
42 | struct KeyboardRow {
43 | let topKeys: [String]
44 | let centerKeys: [String]
45 | let bottomKeys: [String]
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/Enum/ScreenType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenType.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | enum ScreenType: Equatable {
10 |
11 | case main
12 | case addTodoItem
13 | case editTodoItem(id: UUID)
14 |
15 | var screenName: String {
16 | switch self {
17 | case .main:
18 | return "main"
19 | case .addTodoItem:
20 | return "addTodoItem"
21 | case .editTodoItem:
22 | return "editTodoItem"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/Enum/WidgetError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetError.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | enum WidgetError: String, Error {
10 | case unknown
11 | case todoItemDeletionFailure
12 | case todoItemEditingFailure
13 | case todoItemFetchingFailure
14 | case todoItemNameLimitExceeded
15 |
16 | struct Info {
17 | let code: Int
18 | let title: LocalizedStringKey
19 | let message: LocalizedStringKey
20 | }
21 |
22 | var info: Info {
23 | switch self {
24 | case .unknown:
25 | return .init(code: 0,
26 | title: "Unknown Error",
27 | message: "An unknown error occurred.")
28 | case .todoItemDeletionFailure:
29 | return .init(code: 1,
30 | title: "Unable to delete the TODO item",
31 | message: "An error occurred while deleting the item.")
32 | case .todoItemEditingFailure:
33 | return .init(code: 2,
34 | title: "Unable to edit the TODO item",
35 | message: "An error occurred while editing the item.")
36 | case .todoItemFetchingFailure:
37 | return .init(code: 3,
38 | title: "Failed to Fetch TODO Items",
39 | message: "An error occurred while fetching TODO items.")
40 | case .todoItemNameLimitExceeded:
41 | return .init(code: 4,
42 | title: "TODO Item Name Limit Exceeded",
43 | message: "The item name exceeds the allowed character limit of \(WidgetConfig.todoItemNameLimitCount) characters. Please shorten this.")
44 | }
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Model/ListDisplayControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListDisplayControl.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | struct ListDisplayControl {
10 |
11 | init(currentIndex: Int, items: [TodoItem]) {
12 | self.currentIndex = currentIndex
13 | self.items = items
14 | }
15 |
16 | private let currentIndex: Int
17 | private let items: [TodoItem]
18 | private let displayLimitCount = WidgetConfig.displayTodoItemLimitCount
19 |
20 | var canAppearScrollButtons: Bool {
21 | return items.count > displayLimitCount
22 | }
23 |
24 | var displayItems: [TodoItem] {
25 | if items.count <= displayLimitCount {
26 | return items
27 | }
28 |
29 | let endIndex = min(currentIndex + displayLimitCount, items.count)
30 | let startIndex = min(currentIndex, endIndex - displayLimitCount)
31 | let displayItems = Array(items[startIndex.. displayLimitCount,
38 | currentIndex > 0 {
39 | return false
40 | }
41 | return true
42 |
43 | }
44 |
45 | var isDisableScrollDownButton: Bool {
46 | if items.count <= displayLimitCount {
47 | return true
48 | }
49 | return items.count <= currentIndex + displayLimitCount
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Repository/KeyboardInputRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardInputRepository.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | class KeyboardInputRepository {
10 |
11 | init(store: UserDefaultsStore = .shared) {
12 | self.store = store
13 | }
14 |
15 | private let emojiKeyboardContents = EmojiKeyboardContent()
16 | private let store: UserDefaultsStore
17 |
18 | var currentEmojiCategory: EmojiKeyboardContent.Category {
19 | let index = store.emojiKeyboardIndex
20 | return emojiKeyboardContents.getCategory(for: index)
21 | }
22 |
23 | var currentEmojiContent: [String] {
24 | let index = store.emojiKeyboardIndex
25 | if index > 0,
26 | index <= emojiKeyboardContents.keyboardEndIndex {
27 | return emojiKeyboardContents.getEmojis(for: index)
28 | }
29 | return .init(store.frequentlyUsedEmojis)
30 | }
31 |
32 | var emojiKeyboardIndex: Int {
33 | return store.emojiKeyboardIndex
34 | }
35 |
36 | var inputMode: KeyboardInputMode {
37 | return store.keyboardInputMode
38 | }
39 |
40 | var inputText: String {
41 | return store.inputText
42 | }
43 |
44 | var isCapsLocked: Bool {
45 | return store.isCapsLocked
46 | }
47 |
48 | var isEmojiLastContent: Bool {
49 | return store.emojiKeyboardIndex == emojiKeyboardContents.keyboardEndIndex
50 | }
51 |
52 | func appendFrequentlyUsedEmoji(_ emoji: String) {
53 |
54 | store.frequentlyUsedEmojis.removeAll(where: { $0 == emoji })
55 | if store.frequentlyUsedEmojis.count == WidgetConfig.emojiKeyboardContentLimitCount {
56 | store.frequentlyUsedEmojis.removeLast()
57 | }
58 | store.frequentlyUsedEmojis.insert(emoji, at: 0)
59 | }
60 |
61 | func clearInputText() {
62 | return store.inputText = ""
63 | }
64 |
65 | func input(_ text: String) {
66 | store.inputText += text
67 | }
68 |
69 | func changeMode(into mode: KeyboardInputMode) {
70 | store.keyboardInputMode = mode
71 | }
72 |
73 | func deleteLastCharacter() {
74 | var inputText = store.inputText
75 | guard !inputText.isEmpty
76 | else { return }
77 | inputText.removeLast()
78 | store.inputText = inputText
79 | }
80 |
81 | func goBackEmojiContent() {
82 | let currentIndex = store.emojiKeyboardIndex
83 | if currentIndex == emojiKeyboardContents.keyboardStartIndex {
84 | return
85 | }
86 | let index = currentIndex - 1
87 | store.emojiKeyboardIndex = index
88 | }
89 |
90 | func goForwardEmojiContent() {
91 | let currentIndex = store.emojiKeyboardIndex
92 | if currentIndex == emojiKeyboardContents.keyboardEndIndex {
93 | return
94 | }
95 | let index = currentIndex + 1
96 | store.emojiKeyboardIndex = index
97 | }
98 |
99 | func moveEmojiContent(for index: Int) {
100 | store.emojiKeyboardIndex = index
101 | }
102 |
103 | func toggleCapsLock(to isCapsLocked: Bool? = nil) {
104 |
105 | if let isCapsLocked {
106 | store.isCapsLocked = isCapsLocked
107 | } else {
108 | store.isCapsLocked.toggle()
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Repository/ListDisplayRepository.swift:
--------------------------------------------------------------------------------
1 | ////
2 | //// ListDisplayRepository.swift
3 | //// UltimateWidgetTodoWidgetExtension
4 | ////
5 | ////
6 |
7 | import Foundation
8 |
9 | class ListDisplayRepository {
10 |
11 | init(store: UserDefaultsStore = .shared) {
12 | self.store = store
13 | }
14 |
15 | private let store: UserDefaultsStore
16 |
17 | var currentIndex: Int {
18 | return store.listDisplayIndex
19 | }
20 |
21 | func scrollDownList() {
22 | store.listDisplayIndex += 1
23 | }
24 |
25 | func scrollUpList() {
26 | guard store.listDisplayIndex > 0
27 | else { return }
28 | store.listDisplayIndex -= 1
29 | }
30 |
31 | func updateIndex(to index: Int) {
32 | store.listDisplayIndex = index
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Repository/ScreenStateRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenStateRepository.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | class ScreenStateRepository {
10 |
11 | init(store: UserDefaultsStore = .shared) {
12 | self.store = store
13 | }
14 |
15 | private let store: UserDefaultsStore
16 |
17 | var currentScreen: ScreenType {
18 | return store.screenType
19 | }
20 |
21 | var error: WidgetError? {
22 | return store.error
23 | }
24 |
25 | func changeScreen(into type: ScreenType) {
26 | store.screenType = type
27 | }
28 |
29 | func setError(_ error: WidgetError) {
30 | store.error = error
31 | }
32 |
33 | func resetError() {
34 | store.error = nil
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Repository/TodoItemRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItemRepository.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 | import SwiftData
9 |
10 | class TodoItemRepository {
11 |
12 | init(store: SwiftDataStore = .shared) {
13 | self.store = store
14 | }
15 |
16 | private let store: SwiftDataStore
17 |
18 | var container: ModelContainer {
19 | store.container
20 | }
21 |
22 | @MainActor
23 | func addItem(name: String, createDate: Date = Date()) {
24 | let newItem = TodoItem(name: name, createDate: createDate)
25 | store.context.insert(newItem)
26 | }
27 |
28 | @MainActor
29 | func deleteItem(id: UUID) throws {
30 | let item = try fetchItem(id: id)
31 | store.context.delete(item)
32 | }
33 |
34 | @MainActor
35 | func fetchItem(id: UUID) throws -> TodoItem {
36 | let descriptor = FetchDescriptor(predicate: #Predicate { $0.itemId == id })
37 | if let item = try store.context.fetch(descriptor).first {
38 | return item
39 | } else {
40 | throw WidgetError.todoItemFetchingFailure
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/EditTodoItem/AddItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddItemView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 | import WidgetKit
9 |
10 | struct AddItemView: View {
11 |
12 | @Environment(\.widgetTodoCore) var core
13 |
14 | var body: some View {
15 | VStack(spacing: 0) {
16 |
17 | Rectangle()
18 | .fill(.black.opacity(0.001))
19 | .frame(height: WidgetConfig.colorHeaderHeight + 16)
20 |
21 | VStack(spacing: 0) {
22 |
23 | HStack {
24 | CloseButton(type: .addNewTodoItem)
25 | .frame(width: WidgetConfig.topBarHeight,
26 | height: WidgetConfig.topBarHeight)
27 |
28 | Spacer()
29 |
30 | Text(EditTodoItemType.addNewTodoItem.displayLabel)
31 | .font(.system(size: 20))
32 | .bold()
33 | .foregroundStyle(Color.label)
34 |
35 | Spacer()
36 |
37 | Spacer()
38 | .frame(width: WidgetConfig.topBarHeight,
39 | height: WidgetConfig.topBarHeight)
40 | }
41 | .frame(height: WidgetConfig.topBarHeight)
42 |
43 | Spacer()
44 |
45 | InputForm(text: WidgetTodoCore.shared.inputText)
46 |
47 | Spacer().frame(height: 8)
48 | WidgetKeyboard(type: .addNewTodoItem)
49 | }
50 | .background(.widgetBackground)
51 | .clipShape(.rect(cornerRadius: 12, style: .circular))
52 | .contentTransition(.identity)
53 | }
54 | .background(.widgetBackground.opacity(0.001))
55 | }
56 | }
57 |
58 |
59 | // MARK: - Preview
60 | #if DEBUG
61 | struct AddItemPreviewWidget: Widget {
62 | let kind: String = "UltimateWidgetTodo"
63 |
64 | var body: some WidgetConfiguration {
65 | StaticConfiguration(
66 | kind: kind,
67 | provider: WidgetTodoProvider()
68 | ) { entry in
69 | AddItemView()
70 | .containerBackground(for: .widget) {
71 | Color.black
72 | }
73 | }
74 | }
75 | }
76 |
77 | #Preview(as: .systemLarge) {
78 | AddItemPreviewWidget()
79 | } timeline: {
80 | WidgetTodoEntry(date: .now)
81 | }
82 | #endif
83 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/EditTodoItem/Components/CloseButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackButton.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct CloseButton: View {
11 |
12 | let type: EditTodoItemType
13 |
14 | var body: some View {
15 |
16 | Button(intent: CloseEditItemViewButtonIntent()) {
17 | Image(systemName: type.closeButtonImageName)
18 | .font(.system(size: 24))
19 | }
20 | .foregroundStyle(.blue)
21 | .buttonStyle(.plain)
22 | }
23 | }
24 |
25 | #Preview {
26 | HStack {
27 | CloseButton(type: .addNewTodoItem)
28 | CloseButton(type: .editTodoItem(id: UUID()))
29 | }
30 | }
31 |
32 | struct CloseEditItemViewButtonIntent: AppIntent {
33 |
34 | static var title: LocalizedStringResource = "Close Edit Item View Button"
35 |
36 | func perform() async throws -> some IntentResult {
37 | WidgetTodoCore.shared.onTapCloseEditItemViewButton()
38 | return .result()
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/EditTodoItem/Components/InputForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputForm.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct InputForm: View {
10 |
11 | let text: String
12 |
13 | var body: some View {
14 | RoundedRectangle(cornerRadius: 6)
15 | .stroke(lineWidth: 1)
16 | .foregroundStyle(.keyboardBackground)
17 | .frame(width: 296, height: 40)
18 | .overlay {
19 |
20 | HStack(spacing: 0) {
21 |
22 | if text.isEmpty {
23 | highlightBar
24 | .padding(8)
25 | }
26 |
27 | Text(text.isEmpty ? "Add your task here.." : text)
28 | .font(.system(size: 16))
29 | .foregroundStyle(text.isEmpty ? Color.placeholderGray : .label)
30 | .padding(text.isEmpty ? 0 : 8)
31 | .offset(x: text.isEmpty ? -6 : 0)
32 |
33 | if !text.isEmpty {
34 | highlightBar
35 | .offset(x: -6)
36 | }
37 |
38 | Spacer()
39 | }
40 | .contentTransition(.identity)
41 | .frame(width: 296)
42 | }
43 | }
44 |
45 | private var highlightBar: some View {
46 | RoundedRectangle(cornerRadius: 8)
47 | .fill(.blue.opacity(0.8))
48 | .frame(width: 4, height: 26)
49 | }
50 | }
51 |
52 | #Preview {
53 | VStack {
54 | InputForm(text: "")
55 | InputForm(text: "coffeecoffeecoffeecoffeecoffeecoffeecoffeecoffee")
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/EditTodoItem/EditItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditItemView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 | import WidgetKit
9 |
10 | struct EditItemView: View {
11 |
12 | @Environment(\.widgetTodoCore) var core
13 |
14 | let type: EditTodoItemType
15 |
16 | var body: some View {
17 |
18 | WidgetBackgroundView(needsBottomSpacer: false) {
19 |
20 | VStack(spacing: 0) {
21 |
22 | HStack {
23 | CloseButton(type: type)
24 | .frame(width: WidgetConfig.topBarHeight,
25 | height: WidgetConfig.topBarHeight)
26 |
27 | Spacer()
28 |
29 | Text(type.displayLabel)
30 | .font(.system(size: 20))
31 | .bold()
32 | .foregroundStyle(Color.label)
33 |
34 | Spacer()
35 |
36 | Spacer()
37 | .frame(width: WidgetConfig.topBarHeight,
38 | height: WidgetConfig.topBarHeight)
39 | }
40 | .frame(height: WidgetConfig.topBarHeight)
41 |
42 | Line()
43 | .stroke(style: .init(lineWidth: 1))
44 | .foregroundStyle(.gray)
45 | .frame(height: 1)
46 | .shadow(color: .gray, radius: 1, x: 0, y: 1)
47 |
48 | Spacer()
49 |
50 | InputForm(text: WidgetTodoCore.shared.inputText)
51 |
52 | Spacer().frame(height: 8)
53 |
54 | WidgetKeyboard(type: type)
55 | }
56 | }
57 | }
58 | }
59 |
60 | // MARK: - Preview
61 | #if DEBUG
62 | struct EditItemPreviewWidget: Widget {
63 | let kind: String = "UltimateWidgetTodo"
64 |
65 | var body: some WidgetConfiguration {
66 | StaticConfiguration(
67 | kind: kind,
68 | provider: WidgetTodoProvider()
69 | ) { entry in
70 | EditItemView(type: .addNewTodoItem)
71 | .modelContainer(SwiftDataStore.testStore.container)
72 | }
73 | }
74 | }
75 |
76 | #Preview(as: .systemLarge) {
77 | EditItemPreviewWidget()
78 | } timeline: {
79 | WidgetTodoEntry(date: .now)
80 | }
81 | #endif
82 |
83 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/ErrorView/Components/ErrorOKButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorOKButton.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct ErrorOKButton: View {
11 |
12 | var body: some View {
13 |
14 | Button(intent: ErrorOKButtonIntent()) {
15 | Rectangle()
16 | .fill(.keyboardBackground.opacity(0.001))
17 | .overlay {
18 | Text("OK")
19 | .font(.system(size: 26))
20 | .bold()
21 | .foregroundStyle(.blue)
22 | }
23 |
24 | }
25 | .foregroundStyle(.blue)
26 | .buttonStyle(.plain)
27 | }
28 | }
29 |
30 | #Preview {
31 | ErrorOKButton()
32 | }
33 |
34 | struct ErrorOKButtonIntent: AppIntent {
35 |
36 | static var title: LocalizedStringResource = "Error OK Button"
37 |
38 | func perform() async throws -> some IntentResult {
39 | WidgetTodoCore.shared.onTapErrorOKButton()
40 | return .result()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/ErrorView/WidgetTodoErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTodoErrorView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct WidgetTodoErrorView: View {
10 |
11 | init(error: WidgetError, @ViewBuilder content: () -> Content) {
12 | self.content = content()
13 | self.error = error
14 | }
15 |
16 | let content: Content
17 | let error: WidgetError
18 |
19 | var body: some View {
20 |
21 | Color.black.opacity(0.7)
22 | .overlay {
23 |
24 | RoundedRectangle(cornerRadius: 8)
25 | .fill(.keyboardBackground)
26 | .frame(width: 284, height: 284)
27 | .overlay {
28 | VStack(spacing: 8){
29 |
30 | Spacer().frame(height: 8)
31 |
32 | Text(error.info.title)
33 | .font(.system(size: 26))
34 | .bold()
35 | .padding(.horizontal)
36 | Text(error.info.message)
37 | .font(.system(size: 18))
38 | .padding(.horizontal)
39 |
40 | Spacer()
41 |
42 | VStack(spacing: 0) {
43 |
44 | Divider()
45 | .background(.keyShadow)
46 |
47 | Spacer().frame(height: 4)
48 |
49 | ErrorOKButton()
50 | .frame(height: 60)
51 |
52 | Spacer().frame(height: 4)
53 | }
54 | }
55 | .foregroundStyle(Color.label)
56 | .multilineTextAlignment(.center)
57 | }
58 | }
59 | .background(content)
60 | .transition(.opacity)
61 | }
62 | }
63 |
64 | extension View {
65 |
66 | @ViewBuilder
67 | func alert(widgetError: WidgetError?) -> some View {
68 | if let widgetError {
69 | WidgetTodoErrorView(error: widgetError) {
70 | self
71 | }
72 | } else {
73 | self
74 | }
75 | }
76 | }
77 |
78 | #Preview {
79 | WidgetTodoErrorView(error: .todoItemNameLimitExceeded) {
80 | Text("content")
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/AlphabetKeyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlphabetKeyboard.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct AlphabetKeyboard: View {
10 |
11 | @Environment(\.widgetTodoCore) var core
12 |
13 | let type: EditTodoItemType
14 |
15 | var body: some View {
16 |
17 | VStack(spacing: 10) {
18 | HStack(spacing: 6) {
19 | ForEach(core.keyboardTopRowKeys, id: \.self) {
20 | KeyboardLetterKey($0, isCapsLocked: core.isCapsLocked)
21 | .frame(width: 26, height: 34)
22 | }
23 | }
24 |
25 | HStack(spacing: 6) {
26 | ForEach(core.keyboardCenterRowKeys, id: \.self) {
27 | KeyboardLetterKey($0, isCapsLocked: core.isCapsLocked)
28 | .frame(width: 26, height: 34)
29 | }
30 | }
31 |
32 | HStack(spacing: 6) {
33 |
34 | HStack {
35 | CapsLockKey(isCapsLocked: core.isCapsLocked)
36 | .frame(width: 36, height: 34)
37 | Spacer().frame(width: 6)
38 | }
39 |
40 | ForEach(core.keyboardBottomRowKeys, id: \.self) {
41 | KeyboardLetterKey($0, isCapsLocked: core.isCapsLocked)
42 | .frame(width: 26, height: 34)
43 | }
44 |
45 | HStack {
46 | Spacer().frame(width: 6)
47 | BackspaceKey()
48 | .frame(width: 36, height: 34)
49 | }
50 | }
51 |
52 | HStack(spacing: 4) {
53 |
54 | NumberModeKey()
55 | .frame(width: 36, height: 34)
56 | EmojiModeKey()
57 | .frame(width: 36, height: 34)
58 | SpaceKey()
59 | .frame(width: 154, height: 34)
60 | DoneKey(inputText: core.inputText, type: type)
61 | .frame(width: 74, height: 34)
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/EmojiKeyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiKeyboard.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct EmojiKeyboard: View {
10 |
11 | @Environment(\.widgetTodoCore) var core
12 |
13 | var body: some View {
14 |
15 | VStack(spacing: 4) {
16 |
17 | HStack {
18 |
19 | ZStack(alignment: .leading) {
20 | if !core.isEmojiFirstContent {
21 | EmojiContentMoveKey(type: .back)
22 | }
23 | }
24 | .frame(width: 20)
25 |
26 | Spacer()
27 |
28 | Text(core.currentEmojiCategory.info.title)
29 | .font(.system(size: 11))
30 | .bold()
31 | .foregroundStyle(Color.placeholderGray)
32 |
33 | Spacer()
34 |
35 | ZStack(alignment: .trailing) {
36 | if !core.isEmojiLastContent {
37 | EmojiContentMoveKey(type: .forward)
38 | }
39 | }
40 | .frame(width: 20)
41 | }
42 | .padding(.horizontal, 8)
43 | .padding(.bottom, 8)
44 |
45 | HStack(spacing: 0) {
46 |
47 | if core.currentEmojiContent.count <= 36 {
48 | Spacer().frame(width: 6)
49 | }
50 |
51 | LazyHGrid(rows: Array(repeating: GridItem(), count: 4), spacing: 4) {
52 | ForEach(core.currentEmojiContent, id: \.self) {
53 | KeyboardEmojiKey($0)
54 | .padding(.bottom, 4)
55 | }
56 | }
57 |
58 | if core.currentEmojiContent.count <= 36 {
59 | Spacer()
60 | }
61 | }
62 | .frame(height: 116)
63 |
64 | HStack(spacing: 4) {
65 | AlphabetModeKey(hasKeyShape: false)
66 |
67 | ForEach(EmojiKeyboardContent.Category.allCases) {
68 | EmojiCategoryKey(category: $0,
69 | currentCategory: core.currentEmojiCategory)
70 | }
71 |
72 | BackspaceKey(hasKeyShape: false)
73 | }
74 | }
75 | .frame(width: 300)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/AlphabetModeKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlphabetModeKey.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct AlphabetModeKey: View {
11 |
12 | init(hasKeyShape: Bool = true) {
13 | self.hasKeyShape = hasKeyShape
14 | }
15 |
16 | let hasKeyShape: Bool
17 |
18 | var body: some View {
19 |
20 | Button(intent: AlphabetModeKeyIntent()) {
21 |
22 | ZStack {
23 |
24 | if hasKeyShape {
25 | RoundedRectangle(cornerRadius: 6)
26 | .fill(.keyShadow)
27 | .offset(y: 1)
28 |
29 | RoundedRectangle(cornerRadius: 6)
30 | .fill(.keyDarkGray)
31 | }
32 |
33 | Image(systemName: "abc")
34 | .font(.system(size: 12))
35 | .foregroundStyle(Color.label)
36 | }
37 | }
38 | }
39 | }
40 |
41 | #Preview {
42 | Color.keyboardBackground
43 | .overlay {
44 | AlphabetModeKey()
45 | }
46 | }
47 |
48 |
49 | struct AlphabetModeKeyIntent: AppIntent {
50 |
51 | static var title: LocalizedStringResource = "Alphabet Mode key"
52 |
53 | func perform() async throws -> some IntentResult {
54 | WidgetTodoCore.shared.onTapAlphabetModeKey()
55 | return .result()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/BackspaceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackspaceKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct BackspaceKey: View {
11 |
12 | init(hasKeyShape: Bool = true) {
13 | self.hasKeyShape = hasKeyShape
14 | }
15 |
16 | let hasKeyShape: Bool
17 |
18 | var body: some View {
19 |
20 | Button(intent: BackspaceKeyIntent()) {
21 |
22 | ZStack {
23 |
24 | if hasKeyShape {
25 | RoundedRectangle(cornerRadius: 6)
26 | .fill(.keyShadow)
27 | .offset(y: 1)
28 |
29 | RoundedRectangle(cornerRadius: 6)
30 | .fill(.keyDarkGray)
31 | }
32 |
33 | Image(systemName: "delete.backward")
34 | .font(.system(size: 20))
35 | .foregroundStyle(Color.label)
36 | }
37 | }
38 | }
39 | }
40 |
41 | #Preview {
42 | Color.keyboardBackground
43 | .overlay {
44 | BackspaceKey()
45 | }
46 | }
47 |
48 |
49 | struct BackspaceKeyIntent: AppIntent {
50 |
51 | static var title: LocalizedStringResource = "Backspace key"
52 |
53 | func perform() async throws -> some IntentResult {
54 | WidgetTodoCore.shared.onTapBackspaceKey()
55 | return .result()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/CapsLockKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CapsLockKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct CapsLockKey: View {
11 |
12 | init(isCapsLocked: Bool) {
13 | self.isCapsLocked = isCapsLocked
14 | }
15 |
16 | let isCapsLocked: Bool
17 |
18 | var body: some View {
19 |
20 | Button(intent: CapsLockKeyIntent()) {
21 | ZStack {
22 | RoundedRectangle(cornerRadius: 6)
23 | .fill(.keyShadow)
24 | .offset(y: 1)
25 |
26 | RoundedRectangle(cornerRadius: 6)
27 | .fill(isCapsLocked ? .staticKeyWhite : .keyDarkGray)
28 | }
29 | .overlay {
30 | Image(systemName: isCapsLocked ? "arrowshape.left.fill": "arrowshape.left")
31 | .resizable()
32 | .frame(width: 24)
33 | .rotationEffect(.degrees(90))
34 | .scaleEffect(0.6)
35 | .foregroundStyle(isCapsLocked ? .black : Color.label)
36 | }
37 | }
38 | }
39 | }
40 |
41 | #Preview {
42 | Color.gray
43 | .overlay {
44 | HStack {
45 | CapsLockKey(isCapsLocked: true)
46 | CapsLockKey(isCapsLocked: false)
47 | }
48 | }
49 | }
50 |
51 | struct CapsLockKeyIntent: AppIntent {
52 |
53 | static var title: LocalizedStringResource = "Caps lock key"
54 |
55 | func perform() async throws -> some IntentResult {
56 | WidgetTodoCore.shared.onTapCapsLockKey()
57 | return .result()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/DoneKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DoneKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | struct DoneKey: View {
12 |
13 | init(inputText: String, type: EditTodoItemType) {
14 | self.inputText = inputText
15 | self.type = type
16 | }
17 |
18 | let inputText: String
19 | let type: EditTodoItemType
20 |
21 | private var DoneKeyIntent: any AppIntent {
22 | switch type {
23 | case .addNewTodoItem:
24 | return AddItemDoneKeyIntent()
25 | case .editTodoItem(let id):
26 | return EditItemDoneKeyIntent(itemId: id)
27 | }
28 | }
29 |
30 | var body: some View {
31 |
32 | Button(intent: DoneKeyIntent) {
33 | ZStack {
34 | RoundedRectangle(cornerRadius: 6)
35 | .fill(.keyShadow)
36 | .offset(y: 1)
37 |
38 | RoundedRectangle(cornerRadius: 6)
39 | .fill(inputText.isEmpty ? .keyDarkGray : .blue)
40 | }
41 | .overlay {
42 | Text("done")
43 | .font(.system(size: 14))
44 | .bold()
45 | .foregroundStyle(inputText.isEmpty ? .gray : .white)
46 | }
47 | }
48 | }
49 | }
50 |
51 | #Preview {
52 | Color.keyboardBackground
53 | .overlay {
54 | HStack {
55 | DoneKey(inputText: "", type: .addNewTodoItem)
56 | DoneKey(inputText: "something", type: .addNewTodoItem)
57 | }
58 | }
59 | }
60 |
61 |
62 | struct AddItemDoneKeyIntent: AppIntent {
63 |
64 | static var title: LocalizedStringResource = "Add Item Done key"
65 |
66 | func perform() async throws -> some IntentResult {
67 | do {
68 | try await WidgetTodoCore.shared.onTapAddItemDoneKey()
69 | } catch {
70 | let widgetError = error as? WidgetError
71 | WidgetTodoCore.shared.showError(widgetError ?? .unknown)
72 | }
73 | return .result()
74 | }
75 | }
76 |
77 | struct EditItemDoneKeyIntent: AppIntent {
78 |
79 | static var title: LocalizedStringResource = "Edit Item Done key"
80 |
81 | @Parameter(title: "Edit Item ID")
82 | var id: String
83 |
84 | init() {}
85 |
86 | init(itemId: UUID) {
87 | id = itemId.uuidString
88 | }
89 |
90 | func perform() async throws -> some IntentResult {
91 |
92 | if let uuid = UUID(uuidString: id) {
93 | do {
94 | try await WidgetTodoCore.shared.onTapEditItemDoneKey(id: uuid)
95 | } catch {
96 | let widgetError = error as? WidgetError
97 | WidgetTodoCore.shared.showError(widgetError ?? .todoItemEditingFailure)
98 | }
99 | }
100 | return .result()
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/EmojiCategoryKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiCategoryKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct EmojiCategoryKey: View {
11 |
12 | let category: EmojiKeyboardContent.Category
13 | let currentCategory: EmojiKeyboardContent.Category
14 |
15 | private var isCategorySelected: Bool {
16 | return category == currentCategory
17 | }
18 |
19 | var body: some View {
20 | Button(intent: EmojiCategoryKeyIntent(category: category)) {
21 | Text(category.info.icon)
22 | .font(.system(size: 12))
23 | .opacity(isCategorySelected ? 0.8 : 0.4)
24 | .background {
25 | if isCategorySelected {
26 | Circle()
27 | .fill(.keyDarkGray)
28 | .frame(width: 22, height: 22)
29 | }
30 | }
31 | .frame(width: 22, height: 22)
32 | }
33 | }
34 | }
35 |
36 | #Preview {
37 |
38 | Color.emojiKeyboardBackground
39 | .overlay {
40 | HStack {
41 | EmojiCategoryKey(category: .smilyAndPeople,
42 | currentCategory: .smilyAndPeople)
43 | EmojiCategoryKey(category: .animalsAndNature,
44 | currentCategory: .activity)
45 | }
46 | }
47 | }
48 |
49 | struct EmojiCategoryKeyIntent: AppIntent {
50 |
51 | static var title: LocalizedStringResource = "Emoji Category Key"
52 |
53 | @Parameter(title: "Emoji Category ID")
54 | var id: String
55 |
56 | init() {}
57 |
58 | init(category: EmojiKeyboardContent.Category) {
59 | self.id = category.rawValue
60 | }
61 |
62 | func perform() async throws -> some IntentResult {
63 | if let category = EmojiKeyboardContent.Category(rawValue: id) {
64 | WidgetTodoCore.shared.onTapEmojiCategoryKey(category)
65 | }
66 | return .result()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/EmojiContentMoveKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiRightPageKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct EmojiContentMoveKey: View {
11 |
12 | enum Direction {
13 | case back
14 | case forward
15 | }
16 |
17 | let type: Direction
18 |
19 | private var moveKeyIntent: any AppIntent {
20 | switch type {
21 | case .back:
22 | return EmojiContentGoBackKeyIntent()
23 | case .forward:
24 | return EmojiContentGoForwardRightKeyIntent()
25 | }
26 | }
27 |
28 | var body: some View {
29 |
30 | Button(intent: moveKeyIntent) {
31 | Image(systemName: type == .back ? "chevron.left" : "chevron.right")
32 | }
33 | }
34 | }
35 |
36 | #Preview {
37 | HStack {
38 | EmojiContentMoveKey(type: .back)
39 | EmojiContentMoveKey(type: .forward)
40 | }
41 | }
42 |
43 | struct EmojiContentGoBackKeyIntent: AppIntent {
44 |
45 | static var title: LocalizedStringResource = "Emoji content go back key"
46 |
47 | func perform() async throws -> some IntentResult {
48 | WidgetTodoCore.shared.onTapGoBackEmojiContentKey()
49 | return .result()
50 | }
51 | }
52 |
53 |
54 | struct EmojiContentGoForwardRightKeyIntent: AppIntent {
55 |
56 | static var title: LocalizedStringResource = "Emoji content go forward key"
57 |
58 | func perform() async throws -> some IntentResult {
59 | WidgetTodoCore.shared.onTapGoForwardEmojiContentKey()
60 | return .result()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/EmojiModeKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiModeKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct EmojiModeKey: View {
11 |
12 | @Environment(\.colorScheme) var colorScheme
13 |
14 | var isLightMode: Bool {
15 | return colorScheme == .light
16 | }
17 |
18 | var body: some View {
19 |
20 | Button(intent: EmojiModeIntent()) {
21 | ZStack {
22 | RoundedRectangle(cornerRadius: 6)
23 | .fill(.keyShadow)
24 | .offset(y: 1)
25 |
26 | RoundedRectangle(cornerRadius: 6)
27 | .fill(.keyDarkGray)
28 | }
29 | .overlay {
30 | Image(systemName: isLightMode ? "face.smiling" : "face.smiling.fill")
31 | .font(.system(size: 18))
32 | .foregroundStyle(Color.label)
33 | }
34 | }
35 | }
36 | }
37 |
38 | #Preview {
39 | Color.keyboardBackground
40 | .overlay {
41 | HStack {
42 | EmojiModeKey()
43 | EmojiModeKey()
44 | .environment(\.colorScheme, .dark)
45 | }
46 | }
47 | }
48 |
49 |
50 | struct EmojiModeIntent: AppIntent {
51 |
52 | static var title: LocalizedStringResource = "Emoji Mode key"
53 |
54 | func perform() async throws -> some IntentResult {
55 | WidgetTodoCore.shared.onTapEmojiModeKey()
56 | return .result()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/ExtraPunctuationMarksKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtraPunctuationMarksKey.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct ExtraPunctuationMarksKey: View {
11 |
12 | var body: some View {
13 |
14 | Button(intent: ExtraPunctuationMarksKeyIntent()) {
15 | ZStack {
16 | RoundedRectangle(cornerRadius: 6)
17 | .fill(.keyShadow)
18 | .offset(y: 1)
19 |
20 | RoundedRectangle(cornerRadius: 6)
21 | .fill(.keyDarkGray)
22 | }
23 | .overlay {
24 | Text("#+-")
25 | .font(.system(size: 13))
26 | .foregroundStyle(Color.label)
27 | }
28 | }
29 | }
30 | }
31 |
32 | #Preview {
33 | Color.keyboardBackground
34 | .overlay {
35 | ExtraPunctuationMarksKey()
36 | }
37 | }
38 |
39 |
40 | struct ExtraPunctuationMarksKeyIntent: AppIntent {
41 |
42 | static var title: LocalizedStringResource = "More punctuation marks key"
43 |
44 | func perform() async throws -> some IntentResult {
45 | WidgetTodoCore.shared.onTapExtraPunctuationMarksKey()
46 | return .result()
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/KeyboardEmojiKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardEmojiKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct KeyboardEmojiKey: View {
11 |
12 | init(_ emoji: String) {
13 | self.emoji = emoji
14 | }
15 |
16 | let emoji: String
17 |
18 | var body: some View {
19 |
20 | Button(intent: KeyboardEmojiKeyIntent(emoji: emoji)) {
21 | Text(emoji)
22 | .font(.system(size: 20))
23 | }
24 | .buttonStyle(.plain)
25 | }
26 | }
27 |
28 | struct KeyboardEmojiKeyIntent: AppIntent {
29 |
30 | static var title: LocalizedStringResource = "Keyboard emoji key"
31 |
32 | @Parameter(title: "Keyboard emoji key value")
33 | var emoji: String
34 |
35 | init() {}
36 |
37 | init(emoji: String) {
38 | self.emoji = emoji
39 | }
40 |
41 | func perform() async throws -> some IntentResult {
42 | WidgetTodoCore.shared.onTapEmojiKey(emoji)
43 | return .result()
44 | }
45 | }
46 |
47 |
48 | #Preview {
49 | Color.emojiKeyboardBackground
50 | .overlay {
51 | HStack {
52 | KeyboardEmojiKey("😃")
53 | KeyboardEmojiKey("🏳️🌈")
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/KeyboardLetterKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLetterKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct KeyboardLetterKey: View {
11 |
12 | init(_ letter: String, isCapsLocked: Bool) {
13 | if isCapsLocked {
14 | self.letter = letter.uppercased()
15 | } else {
16 | self.letter = letter.lowercased()
17 | }
18 | }
19 |
20 | private let letter: String
21 |
22 | var body: some View {
23 |
24 | Button(intent: KeyboardLetterKeyIntent(letter: letter)) {
25 | ZStack {
26 | RoundedRectangle(cornerRadius: 6)
27 | .fill(.keyShadow)
28 | .offset(y: 1)
29 |
30 | RoundedRectangle(cornerRadius: 6)
31 | .fill(.keyWhite)
32 | }
33 | .overlay {
34 | Text(letter)
35 | .foregroundStyle(Color.label)
36 | .font(.system(size: 20))
37 | }
38 | }
39 | }
40 | }
41 |
42 | struct KeyboardLetterKeyIntent: AppIntent {
43 |
44 | static var title: LocalizedStringResource = "Keyboard letter key"
45 |
46 | @Parameter(title: "Keyboard letter key value")
47 | var letter: String
48 |
49 | init() {}
50 |
51 | init(letter: String) {
52 | self.letter = letter
53 | }
54 |
55 | func perform() async throws -> some IntentResult {
56 | WidgetTodoCore.shared.onTapCharacterKey(letter)
57 | return .result()
58 | }
59 | }
60 |
61 |
62 | #Preview {
63 | Color.gray
64 | .overlay {
65 | HStack {
66 | KeyboardLetterKey("A", isCapsLocked: true)
67 | KeyboardLetterKey("A", isCapsLocked: false)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/NumberModeKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberModeKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct NumberModeKey: View {
11 |
12 | var body: some View {
13 |
14 | Button(intent: NumberModeIntent()) {
15 | ZStack {
16 | RoundedRectangle(cornerRadius: 6)
17 | .fill(.keyShadow)
18 | .offset(y: 1)
19 |
20 | RoundedRectangle(cornerRadius: 6)
21 | .fill(.keyDarkGray)
22 | }
23 | .overlay {
24 | Image(systemName: "textformat.123")
25 | .font(.system(size: 14))
26 | .foregroundStyle(Color.label)
27 | }
28 | }
29 | }
30 | }
31 |
32 | #Preview {
33 | Color.keyboardBackground
34 | .overlay {
35 | NumberModeKey()
36 | }
37 | }
38 |
39 |
40 | struct NumberModeIntent: AppIntent {
41 |
42 | static var title: LocalizedStringResource = "Number Mode key"
43 |
44 | func perform() async throws -> some IntentResult {
45 | WidgetTodoCore.shared.onTapNumberModeKey()
46 | return .result()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/Keys/SpaceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpaceKey.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct SpaceKey: View {
11 |
12 | var body: some View {
13 |
14 | Button(intent: SpaceKeyIntent()) {
15 | ZStack {
16 | RoundedRectangle(cornerRadius: 6)
17 | .fill(.keyShadow)
18 | .offset(y: 1)
19 |
20 | RoundedRectangle(cornerRadius: 6)
21 | .fill(.keyWhite)
22 | }
23 | .overlay {
24 | Text("space")
25 | .font(.system(size: 14))
26 | .foregroundStyle(Color.label)
27 | }
28 | }
29 | }
30 | }
31 |
32 | #Preview {
33 | Color.keyboardBackground
34 | .overlay {
35 | SpaceKey()
36 | }
37 | }
38 |
39 | struct SpaceKeyIntent: AppIntent {
40 |
41 | static var title: LocalizedStringResource = "Space key"
42 |
43 | func perform() async throws -> some IntentResult {
44 | WidgetTodoCore.shared.onTapCharacterKey(" ")
45 | return .result()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/NumberAndPunctuationMarkKeyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberAndPunctuationMarkKeyboard.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct NumberAndPunctuationMarkKeyboard: View {
10 |
11 | @Environment(\.widgetTodoCore) var core
12 | let type: EditTodoItemType
13 |
14 | var body: some View {
15 |
16 | VStack(spacing: 10) {
17 | HStack(spacing: 6) {
18 | ForEach(core.keyboardTopRowKeys, id: \.self) {
19 | KeyboardLetterKey($0, isCapsLocked: false)
20 | .frame(width: 26, height: 34)
21 | }
22 | }
23 |
24 | HStack(spacing: 6) {
25 | ForEach(core.keyboardCenterRowKeys, id: \.self) {
26 | KeyboardLetterKey($0, isCapsLocked: false)
27 | .frame(width: 26, height: 34)
28 | }
29 | }
30 |
31 | HStack(spacing: 6) {
32 |
33 | HStack {
34 |
35 | if WidgetTodoCore.shared.isNumberMode {
36 | ExtraPunctuationMarksKey()
37 | .frame(width: 36, height: 34)
38 | } else {
39 | NumberModeKey()
40 | .frame(width: 36, height: 34)
41 | }
42 |
43 | Spacer().frame(width: 12)
44 | }
45 |
46 | ForEach(core.keyboardBottomRowKeys, id: \.self) {
47 | KeyboardLetterKey($0, isCapsLocked: false)
48 | .frame(width: 36, height: 34)
49 | }
50 |
51 | HStack {
52 | Spacer().frame(width: 12)
53 | BackspaceKey()
54 | .frame(width: 36, height: 34)
55 | }
56 | }
57 |
58 | HStack(spacing: 4) {
59 |
60 | AlphabetModeKey()
61 | .frame(width: 36, height: 34)
62 | EmojiModeKey()
63 | .frame(width: 36, height: 34)
64 | SpaceKey()
65 | .frame(width: 154, height: 34)
66 | DoneKey(inputText: core.inputText, type: type)
67 | .frame(width: 74, height: 34)
68 | }
69 | }
70 | }
71 | }
72 |
73 | #Preview {
74 | NumberAndPunctuationMarkKeyboard(type: .addNewTodoItem)
75 | }
76 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Keyboard/WidgetKeyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetKeyboard.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 | import WidgetKit
9 |
10 | struct WidgetKeyboard: View {
11 |
12 | @Environment(\.widgetTodoCore) var core
13 | let type: EditTodoItemType
14 |
15 | private var backgroundColor: Color {
16 | return core.keyboardInputMode == .emoji ? .emojiKeyboardBackground : .keyboardBackground
17 | }
18 |
19 | var body: some View {
20 |
21 | backgroundColor
22 | .overlay {
23 | VStack(spacing: 10) {
24 |
25 | switch core.keyboardInputMode {
26 | case .alphabet:
27 | AlphabetKeyboard(type: type)
28 | case .emoji:
29 | EmojiKeyboard()
30 | case .extraPunctuationMarks, .number:
31 | NumberAndPunctuationMarkKeyboard(type: type)
32 | }
33 | }
34 | }
35 | .frame(height: WidgetConfig.keyboardHeight)
36 | .contentTransition(.identity)
37 | .buttonStyle(.plain)
38 | }
39 | }
40 |
41 | #if DEBUG
42 | #Preview(as: .systemLarge) {
43 | EditItemPreviewWidget()
44 | } timeline: {
45 | WidgetTodoEntry(date: .now)
46 | }
47 | #endif
48 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Line.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Line.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct Line: Shape {
10 | func path(in rect: CGRect) -> Path {
11 | var path = Path()
12 | path.move(to: CGPoint(x: 0, y: 0))
13 | path.addLine(to: CGPoint(x: rect.width, y: 0))
14 | return path
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/MainView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftData
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | struct MainView: View {
12 |
13 | @Environment(\.modelContext) private var modelContext
14 | @Query (sort: \TodoItem.createDate, order: .reverse)
15 | private var items: [TodoItem]
16 |
17 | var body: some View {
18 |
19 | WidgetBackgroundView {
20 | Group {
21 | if items.isEmpty {
22 | TodoItemEmptyView()
23 | } else {
24 | TodoItemListView(items: items)
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
31 | // MARK: - Preview
32 | #if DEBUG
33 | struct MainPreviewWidget: Widget {
34 | let kind: String = "UltimateWidgetTodo"
35 |
36 | var body: some WidgetConfiguration {
37 | StaticConfiguration(
38 | kind: kind,
39 | provider: WidgetTodoProvider()
40 | ) { entry in
41 | MainView()
42 | .modelContainer(SwiftDataStore.testStore.container)
43 | }
44 | }
45 | }
46 |
47 | #Preview(as: .systemLarge) {
48 | MainPreviewWidget()
49 | } timeline: {
50 | WidgetTodoEntry(date: .now)
51 | }
52 | #endif
53 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/Components/CompleteTodoItemButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteTodoItemButton.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct CompleteTodoItemButton: View {
11 |
12 | let item: TodoItem
13 |
14 | var body: some View {
15 | Toggle(isOn: false, intent: CompleteTodoItemIntent(item: item)) {
16 | EmptyView()
17 | }
18 | .toggleStyle(TodoItemToggleStyle())
19 | }
20 | }
21 |
22 | struct TodoItemToggleStyle: ToggleStyle {
23 |
24 | func makeBody(configuration: Configuration) -> some View {
25 | Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
26 | .font(.system(size: 24))
27 | .foregroundColor( configuration.isOn ? .mint : .placeholderGray)
28 | }
29 | }
30 |
31 | struct CompleteTodoItemIntent: AppIntent {
32 |
33 | static var title: LocalizedStringResource = "Complete TODO Item"
34 |
35 | @Parameter(title: "Item ID")
36 | var id: String
37 |
38 | init() {}
39 |
40 | init(item: TodoItem) {
41 | self.id = item.itemId.uuidString
42 | }
43 |
44 | func perform() async throws -> some IntentResult {
45 | if let uuid = UUID(uuidString: id) {
46 |
47 | do {
48 | try await WidgetTodoCore.shared.onTapCompleteTodoItem(id: uuid)
49 | } catch {
50 | WidgetTodoCore.shared.showError(.todoItemDeletionFailure)
51 | }
52 | }
53 | return .result()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/Components/ListScrollButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListScrollButton.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 |
10 | struct ListScrollButton: View {
11 |
12 | enum Direction {
13 | case down
14 | case up
15 | }
16 |
17 | let direction: Direction
18 | let isDisabled: Bool
19 |
20 | private var scrollButtonIntent: any AppIntent {
21 | if isDisabled {
22 | return ListScrollDisabledButtonIntent()
23 | }
24 | return direction == .down ? ListScrollDownButtonIntent() : ListScrollUpButtonIntent()
25 | }
26 |
27 | var body: some View {
28 |
29 | Button(intent: scrollButtonIntent) {
30 | Image(systemName: direction == .down ? "chevron.down" : "chevron.up")
31 | .font(.system(size: 22))
32 | .foregroundStyle(isDisabled ? .gray: .blue)
33 | }
34 | .buttonStyle(.plain)
35 | }
36 | }
37 |
38 | #Preview {
39 | HStack {
40 | ListScrollButton(direction: .up,
41 | isDisabled: true)
42 | ListScrollButton(direction: .up,
43 | isDisabled: false)
44 | ListScrollButton(direction: .down,
45 | isDisabled: true)
46 | ListScrollButton(direction: .down,
47 | isDisabled: false)
48 | }
49 | }
50 |
51 | struct ListScrollDownButtonIntent: AppIntent {
52 |
53 | static var title: LocalizedStringResource = "List Scroll Down Button"
54 |
55 | func perform() async throws -> some IntentResult {
56 | WidgetTodoCore.shared.onTapScrollDownList()
57 | return .result()
58 | }
59 | }
60 |
61 | struct ListScrollUpButtonIntent: AppIntent {
62 |
63 | static var title: LocalizedStringResource = "List Scroll Up Button"
64 |
65 | func perform() async throws -> some IntentResult {
66 | WidgetTodoCore.shared.onTapScrollUpList()
67 | return .result()
68 | }
69 | }
70 |
71 | struct ListScrollDisabledButtonIntent: AppIntent {
72 |
73 | static var title: LocalizedStringResource = "List Scroll Disable Button"
74 |
75 | func perform() async throws -> some IntentResult {
76 | print("This does not work")
77 | WidgetTodoCore.shared.onTapDisabledScroll()
78 | return .result()
79 | }
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/Components/PlusButtonImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlusButtonImage.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct PlusButtonImage: View {
10 | var body: some View {
11 | ZStack {
12 | Image(systemName: "circle.fill")
13 | .resizable()
14 | .foregroundStyle(.widgetBackground)
15 | Image(systemName: "plus.circle.fill")
16 | .resizable()
17 | .foregroundStyle(.blue)
18 | }
19 | }
20 | }
21 |
22 | #Preview {
23 | PlusButtonImage()
24 | .frame(width: 44, height: 44)
25 | }
26 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/Components/PresentAddItemViewButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PresentAddItemViewButton.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import AppIntents
8 | import WidgetKit
9 | import SwiftUI
10 |
11 | struct PresentAddItemViewButton: View {
12 |
13 | enum ButtonType {
14 | case floatingAction
15 | case fullScreen
16 | }
17 |
18 | let type: ButtonType
19 |
20 | var body: some View {
21 |
22 | Button(intent: PresentAddItemViewIntent()) {
23 |
24 | switch type {
25 | case .floatingAction:
26 | PlusButtonImage()
27 | case .fullScreen:
28 | Rectangle()
29 | .fill(.white.opacity(0.0001))
30 | }
31 | }
32 | .buttonStyle(.plain)
33 | }
34 | }
35 |
36 | struct PresentAddItemViewIntent: AppIntent {
37 |
38 | static var title: LocalizedStringResource = "Present Add Item view button"
39 |
40 | func perform() async throws -> some IntentResult {
41 | WidgetTodoCore.shared.onTapPresentAddItemView()
42 | return .result()
43 | }
44 | }
45 |
46 | #if DEBUG
47 | #Preview(as: .systemLarge) {
48 | TodoItemListPreviewWidget()
49 | } timeline: {
50 | WidgetTodoEntry(date: .now)
51 | }
52 | #endif
53 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/Components/TodoItemListRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItemListRow.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import AppIntents
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | struct TodoItemListRow: View {
12 |
13 | let item: TodoItem
14 |
15 | var body: some View {
16 |
17 | VStack(spacing: 0) {
18 | HStack {
19 | CompleteTodoItemButton(item: item)
20 |
21 | Button(intent: TodoItemListRowSelectIntent(item: item)) {
22 | Rectangle()
23 | .fill(.widgetBackground.opacity(0.001))
24 | .overlay {
25 |
26 | HStack {
27 | Text(item.name)
28 | .font(.system(size: 16))
29 |
30 | Spacer()
31 |
32 | Image(systemName: "chevron.right")
33 | .font(.system(size: 12))
34 | .foregroundStyle(Color.placeholderGray)
35 | }
36 | }
37 | }
38 | .buttonStyle(.plain)
39 | }
40 | .frame(height: 24)
41 | .padding(.vertical, 8)
42 |
43 | Line()
44 | .stroke(style: .init(dash: [3, 3]))
45 | .foregroundColor(.gray)
46 | .frame(height: 0.5)
47 | .padding(.leading, 32)
48 | }
49 | }
50 | }
51 |
52 | struct TodoItemListRowSelectIntent: AppIntent {
53 |
54 | static var title: LocalizedStringResource = "TODO Item List Row"
55 |
56 | @Parameter(title: "Item ID")
57 | var id: String
58 |
59 | @Parameter(title: "Item name")
60 | var name: String
61 |
62 | init() {}
63 |
64 | init(item: TodoItem) {
65 | self.id = item.itemId.uuidString
66 | self.name = item.name
67 | }
68 |
69 | func perform() async throws -> some IntentResult {
70 | if let uuid = UUID(uuidString: id) {
71 | WidgetTodoCore.shared.onTapTodoItemListRow(id: uuid, name: name)
72 | }
73 | return .result()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/TodoItemEmptyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItemEmptyView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 |
8 | import SwiftUI
9 |
10 | struct TodoItemEmptyView: View {
11 |
12 | var body: some View {
13 | ZStack {
14 | VStack(spacing: 32) {
15 |
16 | Spacer()
17 |
18 | VStack(spacing: 8) {
19 | Text("What's your task?")
20 | .font(.system(size: 32))
21 | .bold()
22 | .foregroundStyle(LinearGradient.ultimateBlue)
23 |
24 | Text("Start by adding a new one.")
25 | .font(.system(size: 20))
26 | .fontWeight(.light)
27 | }
28 |
29 | PlusButtonImage()
30 | .frame(width: 80, height: 80)
31 |
32 | Spacer()
33 | }
34 | PresentAddItemViewButton(type: .fullScreen)
35 | }
36 | }
37 | }
38 |
39 | // MARK: - Preview
40 | #if DEBUG
41 | import WidgetKit
42 |
43 | struct TodoItemEmptyPreviewWidget: Widget {
44 | let kind: String = "UltimateWidgetTodo"
45 |
46 | var body: some WidgetConfiguration {
47 | StaticConfiguration(
48 | kind: kind,
49 | provider: WidgetTodoProvider()
50 | ) { entry in
51 | WidgetBackgroundView {
52 | TodoItemEmptyView()
53 | }
54 | .modelContainer(SwiftDataStore.testStore.container)
55 | }
56 | }
57 | }
58 |
59 | #Preview(as: .systemLarge) {
60 | TodoItemEmptyPreviewWidget()
61 | } timeline: {
62 | WidgetTodoEntry(date: .now)
63 | }
64 | #endif
65 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Views/Main/TodoItemList/TodoItemListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItemListView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 | import WidgetKit
9 |
10 | struct TodoItemListView: View {
11 |
12 | @Environment(\.widgetContentMargins) var margins
13 | @Environment(\.widgetTodoCore) var core
14 | let items: [TodoItem]
15 |
16 | private var listDisplayControl: ListDisplayControl {
17 | return core.makeListDisplayControl(for: items)
18 | }
19 |
20 | var body: some View {
21 | ZStack {
22 | VStack(spacing: 0) {
23 |
24 | HStack(spacing: 0) {
25 |
26 | HStack(alignment: .bottom, spacing: 4) {
27 | Text(String(items.count))
28 | .contentTransition(.numericText())
29 | .font(.system(size: 30))
30 | .bold()
31 | Text("tasks")
32 | .font(.system(size: 13))
33 | .fontWeight(.semibold)
34 | .offset(y: -4)
35 | }
36 | .foregroundStyle(Color.label)
37 | .padding(.leading, margins.leading)
38 |
39 | Spacer()
40 |
41 | if listDisplayControl.canAppearScrollButtons {
42 |
43 | Group {
44 | ListScrollButton(direction: .up,
45 | isDisabled: listDisplayControl.isDisableScrollUpButton)
46 | .frame(width: 44, height: 40)
47 | ListScrollButton(direction: .down,
48 | isDisabled: listDisplayControl.isDisableScrollDownButton)
49 | .frame(width: 44, height: 40)
50 | }
51 | }
52 |
53 | }
54 | .frame(height: WidgetConfig.topBarHeight)
55 |
56 | Line()
57 | .stroke(style: .init(lineWidth: 1))
58 | .foregroundStyle(.gray)
59 | .frame(height: 1)
60 | .shadow(color: .gray, radius: 1, x: 0, y: 1)
61 |
62 | ForEach(listDisplayControl.displayItems) {
63 | TodoItemListRow(item: $0)
64 | }
65 | .id("TodoItemListRows")
66 | .transition(core.listScrollTransition)
67 | .padding(.trailing, margins.trailing)
68 | .padding(.leading, margins.leading)
69 |
70 | Spacer()
71 | }
72 |
73 | VStack {
74 | Spacer()
75 |
76 | HStack {
77 | Spacer()
78 |
79 | PresentAddItemViewButton(type: .floatingAction)
80 | .frame(width: 44, height: 44)
81 | }
82 | }
83 | .padding(.trailing, margins.trailing)
84 | .padding(.bottom, 4)
85 | }
86 | }
87 | }
88 |
89 | // MARK: - Preview
90 | #if DEBUG
91 | import WidgetKit
92 |
93 | struct TodoItemListPreviewWidget: Widget {
94 | let kind: String = "UltimateWidgetTodo"
95 |
96 | var body: some WidgetConfiguration {
97 | StaticConfiguration(
98 | kind: kind,
99 | provider: WidgetTodoProvider()
100 | ) { entry in
101 | WidgetBackgroundView {
102 | TodoItemListView(items: [])
103 | }
104 | .modelContainer(SwiftDataStore.testStore.container)
105 | }
106 | }
107 | }
108 |
109 | #Preview(as: .systemLarge) {
110 | TodoItemListPreviewWidget()
111 | } timeline: {
112 | WidgetTodoEntry(date: .now)
113 | }
114 | #endif
115 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/Contents/UltimateWidgetTodoWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UltimateWidgetTodoWidget.swift
3 | // UltimateWidgetTodoWidget
4 | //
5 | //
6 |
7 | import SwiftData
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | struct UltimateWidgetTodoWidget: Widget {
12 |
13 | let kind: String = "UltimateWidgetTodo"
14 |
15 | var body: some WidgetConfiguration {
16 | StaticConfiguration(kind: kind, provider: WidgetTodoProvider()) { entry in
17 | WidgetTodoView()
18 | }
19 | .supportedFamilies([.systemLarge])
20 | .configurationDisplayName("Ultimate Widget Todo")
21 | .description("This is an Ultimate Todo List App with a Widget.")
22 | .contentMarginsDisabled()
23 | }
24 | }
25 |
26 | #Preview(as: .systemLarge) {
27 | UltimateWidgetTodoWidget()
28 | } timeline: {
29 | WidgetTodoEntry(date: .now)
30 | WidgetTodoEntry(date: .now)
31 | }
32 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/Contents/UltimateWidgetTodoWidgetBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UltimateWidgetTodoWidgetBundle.swift
3 | // UltimateWidgetTodoWidget
4 | //
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 |
10 | @main
11 | struct UltimateWidgetTodoWidgetBundle: WidgetBundle {
12 | var body: some Widget {
13 | UltimateWidgetTodoWidget()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/Contents/WidgetTodoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTodoView.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftUI
8 | import WidgetKit
9 |
10 | struct WidgetTodoView: View {
11 |
12 | @Environment(\.widgetTodoCore) var core
13 |
14 | var body: some View {
15 |
16 | ZStack {
17 | switch core.currentScreen {
18 | case .main, .addTodoItem:
19 |
20 | ZStack {
21 | MainView()
22 | .transition(.opacity)
23 |
24 | if core.currentScreen == .addTodoItem {
25 |
26 | Color.black.opacity(0.7)
27 | .contentTransition(.opacity)
28 |
29 | AddItemView()
30 | .frame(height: core.currentScreen == .addTodoItem ? nil : 0)
31 | .transition(.asymmetric(insertion: .push(from: .top),
32 | removal: .push(from: .top)))
33 | }
34 | }
35 |
36 | case let .editTodoItem(id):
37 | EditItemView(type: .editTodoItem(id: id))
38 | .transition(.asymmetric(insertion: .push(from: .leading),
39 | removal: .push(from: .leading)))
40 | }
41 | }
42 | .alert(widgetError: core.error)
43 | .modelContainer(core.swiftDataContainer)
44 | }
45 | }
46 |
47 | #Preview {
48 | WidgetTodoView()
49 | }
50 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/TodoItemEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTodoEntry.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import WidgetKit
8 |
9 | struct WidgetTodoEntry: TimelineEntry {
10 | let date: Date
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/TodoTaskProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTodoProvider.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import WidgetKit
8 |
9 | struct WidgetTodoProvider: TimelineProvider {
10 | func placeholder(in context: Context) -> WidgetTodoEntry {
11 | WidgetTodoEntry(date: Date())
12 | }
13 |
14 | func getSnapshot(in context: Context, completion: @escaping (WidgetTodoEntry) -> ()) {
15 | let entry = WidgetTodoEntry(date: Date())
16 | completion(entry)
17 | }
18 |
19 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
20 |
21 | var entries: [WidgetTodoEntry] = []
22 |
23 | // Generate a timeline consisting of five entries an hour apart, starting from the current date.
24 | let currentDate = Date()
25 | for hourOffset in 0 ..< 5 {
26 | let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
27 | let entry = WidgetTodoEntry(date: entryDate)
28 | entries.append(entry)
29 | }
30 |
31 | let timeline = Timeline(entries: entries, policy: .atEnd)
32 | completion(timeline)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/WidgetBackground/WidgetBackgroundView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetBackgroundView.swift
3 | // UltimateWidgetTodoWidgetExtension
4 | //
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct WidgetBackgroundView: View {
10 |
11 | init(needsBottomSpacer: Bool = true, @ViewBuilder content: () -> Content) {
12 | self.content = content()
13 | self.needsBottomSpacer = needsBottomSpacer
14 | }
15 |
16 | let content: Content
17 | let needsBottomSpacer: Bool
18 |
19 | var body: some View {
20 | VStack(spacing: 0) {
21 | Rectangle()
22 | .fill(LinearGradient.ultimateBlue)
23 | .frame(height: WidgetConfig.colorHeaderHeight)
24 |
25 | content
26 |
27 | if needsBottomSpacer {
28 | Spacer()
29 | }
30 | }
31 | .background(.widgetBackground)
32 | }
33 | }
34 |
35 | // MARK: - Preview
36 | #if DEBUG
37 | import WidgetKit
38 |
39 | struct WidgetBackgroundPreviewWidget: Widget {
40 | let kind: String = "UltimateWidgetTodo"
41 |
42 | var body: some WidgetConfiguration {
43 | StaticConfiguration(
44 | kind: kind,
45 | provider: WidgetTodoProvider()
46 | ) { entry in
47 | WidgetBackgroundView {
48 | MainView()
49 | }
50 | .modelContainer(SwiftDataStore.testStore.container)
51 | }
52 | }
53 | }
54 |
55 | #Preview(as: .systemLarge) {
56 | WidgetBackgroundPreviewWidget()
57 | } timeline: {
58 | WidgetTodoEntry(date: .now)
59 | }
60 | #endif
61 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/WidgetConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetConfig.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | class WidgetConfig {
10 | static let colorHeaderHeight: CGFloat = 26
11 | static let emojiKeyboardContentLimitCount = 40
12 | static let displayTodoItemLimitCount = 6
13 | static let keyboardHeight: CGFloat = 194
14 | static let todoItemNameLimitCount = 26
15 | static let topBarHeight: CGFloat = 40
16 | }
17 |
--------------------------------------------------------------------------------
/UltimateWidgetTodoWidget/Widget/WidgetTodoCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTodoCore.swift
3 | // UltimateWidgetTodo
4 | //
5 | //
6 |
7 | import SwiftData
8 | import SwiftUI
9 |
10 | class WidgetTodoCore: ObservableObject {
11 |
12 | static let shared = WidgetTodoCore()
13 | static let test = WidgetTodoCore(isTestMode: true)
14 |
15 | private init(isTestMode: Bool = false) {
16 | let userDefaultsStore: UserDefaultsStore = isTestMode ? .testStore : .shared
17 | let swiftDataStore: SwiftDataStore = isTestMode ? .testStore : .shared
18 |
19 | self.keyboardInputRepository = .init(store: userDefaultsStore)
20 | self.listDisplayRepository = .init(store: userDefaultsStore)
21 | self.screenStateRepository = .init(store: userDefaultsStore)
22 | self.todoItemRepository = .init(store: swiftDataStore)
23 | }
24 |
25 | private let keyboardInputRepository: KeyboardInputRepository
26 | private let listDisplayRepository: ListDisplayRepository
27 | private let screenStateRepository: ScreenStateRepository
28 | private let todoItemRepository: TodoItemRepository
29 |
30 | // MARK: - Properties
31 | @Published private(set) var listScrollTransition: AnyTransition = .identity
32 |
33 | var currentEmojiCategory: EmojiKeyboardContent.Category {
34 | return keyboardInputRepository.currentEmojiCategory
35 | }
36 |
37 | var currentEmojiContent: [String] {
38 | return keyboardInputRepository.currentEmojiContent
39 | }
40 |
41 | var currentScreen: ScreenType {
42 | return screenStateRepository.currentScreen
43 | }
44 |
45 | var error: WidgetError? {
46 | return screenStateRepository.error
47 | }
48 |
49 | var inputText: String {
50 | return keyboardInputRepository.inputText
51 | }
52 |
53 | var isCapsLocked: Bool {
54 | return keyboardInputRepository.isCapsLocked
55 | }
56 |
57 | var isEmojiFirstContent: Bool {
58 | return currentEmojiCategory == .frequentlyUsed
59 | }
60 |
61 | var isEmojiLastContent: Bool {
62 | return keyboardInputRepository.isEmojiLastContent
63 | }
64 |
65 | var isEmojiMode: Bool {
66 | return keyboardInputRepository.inputMode == .emoji
67 | }
68 |
69 | var isNumberMode: Bool {
70 | return keyboardInputRepository.inputMode == .number
71 | }
72 |
73 | var keyboardBottomRowKeys: [String] {
74 | return keyboardInputMode.keyboardRow.bottomKeys
75 | }
76 |
77 | var keyboardCenterRowKeys: [String] {
78 | return keyboardInputMode.keyboardRow.centerKeys
79 | }
80 |
81 | var keyboardInputMode: KeyboardInputMode {
82 | return keyboardInputRepository.inputMode
83 | }
84 |
85 | var keyboardTopRowKeys: [String] {
86 | return keyboardInputMode.keyboardRow.topKeys
87 | }
88 |
89 | var swiftDataContainer: ModelContainer {
90 | return todoItemRepository.container
91 | }
92 |
93 | // MARK: - Functions
94 |
95 | func makeListDisplayControl(for items: [TodoItem]) -> ListDisplayControl {
96 | return ListDisplayControl(currentIndex: listDisplayRepository.currentIndex,
97 | items: items)
98 | }
99 |
100 | func onTapAddItemDoneKey() async throws {
101 | let name = keyboardInputRepository.inputText
102 | if name.isEmpty { return }
103 |
104 | guard name.count <= WidgetConfig.todoItemNameLimitCount
105 | else { throw WidgetError.todoItemNameLimitExceeded }
106 |
107 | listDisplayRepository.updateIndex(to: 0)
108 | await todoItemRepository.addItem(name: name)
109 | keyboardInputRepository.clearInputText()
110 |
111 | screenStateRepository.changeScreen(into: .main)
112 | }
113 |
114 | func onTapAlphabetModeKey() {
115 | keyboardInputRepository.changeMode(into: .alphabet)
116 | }
117 |
118 | func onTapBackspaceKey() {
119 | keyboardInputRepository.deleteLastCharacter()
120 | }
121 |
122 | func onTapCapsLockKey() {
123 | keyboardInputRepository.toggleCapsLock()
124 | }
125 |
126 | func onTapCharacterKey(_ character: String) {
127 | keyboardInputRepository.input(character)
128 | }
129 |
130 | func onTapCloseEditItemViewButton() {
131 | keyboardInputRepository.changeMode(into: .alphabet)
132 | keyboardInputRepository.moveEmojiContent(for: 0)
133 | keyboardInputRepository.clearInputText()
134 | screenStateRepository.changeScreen(into: .main)
135 | }
136 |
137 | func onTapCompleteTodoItem(id: UUID) async throws {
138 | try await todoItemRepository.deleteItem(id: id)
139 | listScrollTransition = .identity
140 | }
141 |
142 | func onTapDisabledScroll() {
143 | // need to Disable list animations
144 | listScrollTransition = .identity
145 | }
146 |
147 | func onTapEditItemDoneKey(id: UUID) async throws {
148 | let name = keyboardInputRepository.inputText
149 | if name.isEmpty { return }
150 |
151 | guard name.count <= WidgetConfig.todoItemNameLimitCount
152 | else { throw WidgetError.todoItemNameLimitExceeded }
153 |
154 | let item = try await todoItemRepository.fetchItem(id: id)
155 | item.name = name
156 | item.updateDate = Date()
157 |
158 | keyboardInputRepository.changeMode(into: .alphabet)
159 | keyboardInputRepository.moveEmojiContent(for: 0)
160 | keyboardInputRepository.clearInputText()
161 |
162 | screenStateRepository.changeScreen(into: .main)
163 | }
164 |
165 | func onTapEmojiCategoryKey(_ category: EmojiKeyboardContent.Category) {
166 | keyboardInputRepository.moveEmojiContent(for: category.info.startIndex)
167 | }
168 |
169 | func onTapEmojiKey(_ emoji: String) {
170 | keyboardInputRepository.appendFrequentlyUsedEmoji(emoji)
171 | keyboardInputRepository.input(emoji)
172 | }
173 |
174 | func onTapEmojiModeKey() {
175 | keyboardInputRepository.changeMode(into: .emoji)
176 | }
177 |
178 | func onTapErrorOKButton() {
179 | screenStateRepository.resetError()
180 | }
181 |
182 | func onTapExtraPunctuationMarksKey() {
183 | keyboardInputRepository.changeMode(into: .extraPunctuationMarks)
184 | }
185 |
186 | func onTapGoBackEmojiContentKey() {
187 | keyboardInputRepository.goBackEmojiContent()
188 | }
189 |
190 | func onTapGoForwardEmojiContentKey() {
191 | keyboardInputRepository.goForwardEmojiContent()
192 | }
193 |
194 | func onTapNumberModeKey() {
195 | keyboardInputRepository.changeMode(into: .number)
196 | }
197 |
198 | func onTapPresentAddItemView() {
199 | keyboardInputRepository.toggleCapsLock(to: true)
200 | screenStateRepository.changeScreen(into: .addTodoItem)
201 | }
202 |
203 | func onTapScrollDownList() {
204 | listScrollTransition = .push(from: .bottom)
205 | listDisplayRepository.scrollDownList()
206 | }
207 |
208 | func onTapScrollUpList() {
209 | listScrollTransition = .push(from: .top)
210 | listDisplayRepository.scrollUpList()
211 | }
212 |
213 | func onTapTodoItemListRow(id: UUID, name: String) {
214 | keyboardInputRepository.input(name)
215 | keyboardInputRepository.toggleCapsLock(to: false)
216 | screenStateRepository.changeScreen(into: .editTodoItem(id: id))
217 | }
218 |
219 | func showError(_ error: WidgetError) {
220 | screenStateRepository.setError(error)
221 | }
222 | }
223 |
224 | // MARK: - EnvironmentValues
225 |
226 | struct WidgetTodoCoreEnvironmentKey: EnvironmentKey {
227 | typealias Value = WidgetTodoCore
228 | static var defaultValue: WidgetTodoCore = .shared
229 | }
230 |
231 | extension EnvironmentValues {
232 | var widgetTodoCore: WidgetTodoCore {
233 | get {
234 | self[WidgetTodoCoreEnvironmentKey.self]
235 | }
236 | set {
237 | self[WidgetTodoCoreEnvironmentKey.self] = newValue
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------