├── .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 | [![Download_on_the_App_Store_Badge](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/efca548d-574b-4d32-8fea-aa81f686d322)](https://apps.apple.com/us/app/ultimatewidgettodo/id6471950020) 10 | 11 | ![License](https://img.shields.io/badge/License-MIT-blue.svg) 12 | 13 | ## Demo 14 | 15 | ### Task Management 16 | 17 | Create|Update|Delete 18 | --|--|-- 19 | ![Create](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/410cdeb1-57ae-492f-b23c-ca33ce244742)|![Update](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/a5ea8986-7345-4eb0-9e82-051d11550978)|![Delete](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/5ed4a468-02aa-4f08-9ab1-e827caa9e96b) 20 | 21 | ### Scroll 22 | 23 | Up|Down 24 | --|-- 25 | ![Scroll up](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/38ca3510-7c8c-4db0-9e2b-fa704177178b)|![Scroll down](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/62fa69e4-a1aa-495e-abc4-f918d46a36e7) 26 | 27 | ### Transition 28 | 29 | Push-like|Modal-sheet-like 30 | --|-- 31 | ![Push-like](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/d8081692-0c3d-45f6-a545-1d0ae3c7efa1)|![Modal-sheet-like](https://github.com/littleossa/UltimateWidgetTodo/assets/67716751/bb3b8d9c-1151-4967-9a14-1349d284affe) 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 | --------------------------------------------------------------------------------