Compare commits

...

391 Commits
3.5.3 ... main

Author SHA1 Message Date
ShikiSuen abb17e9ff6 [VersionUp] 3.8.5 GM Build 3850. 2024-04-07 00:56:09 +08:00
ShikiSuen afad2076b0 Update Data - 20240407 2024-04-07 00:26:41 +08:00
ShikiSuen 40245cf1d5 ToolTipUI // Enable round panel corners on old systems. 2024-04-06 17:07:13 +08:00
ShikiSuen 08a54c14c1 PCB // Enable round panel corners. 2024-04-06 16:38:26 +08:00
ShikiSuen 67a6bb5f87 LMMgr && Main // Allow dumping user dicts through terminal. 2024-04-02 19:12:35 +08:00
ShikiSuen 97a9e9aa5c LMAssembly // Let LMInstantiator summarize user data. 2024-04-02 19:12:35 +08:00
ShikiSuen c932083c5f LMInstantiator // Make async optional while loading user dicts. 2024-04-02 18:47:18 +08:00
ShikiSuen 25d8f7c093 LMInstantiator // Stop pinning default user weights for single reading. 2024-03-30 18:38:45 +08:00
ShikiSuen 14adf03311 LMInstantiator // Differentiate scores from factory results. 2024-03-30 18:38:45 +08:00
ShikiSuen 817df50916 LMMgr.UserPhrase // Fine-tweak suggestNextFreq(). 2024-03-30 18:31:06 +08:00
ShikiSuen e8961ff33f SessionCtl // Switch to .ofEmpty() state on toggling CapsLock. 2024-03-21 19:58:41 +08:00
ShikiSuen 2d23deb83a Tekkon // Update to v1.6.0 release. 2024-03-21 16:50:49 +08:00
ShikiSuen fc5243c97f AppDelegate // Update max RAM threshold to 1024MB. 2024-03-21 16:50:46 +08:00
ShikiSuen 9e1d130ba7 UserDef // +kMinCellWidthForHorizontalMatrix. 2024-03-10 22:00:51 +08:00
ShikiSuen 5a6aee2a25 PCB // Tweak panel opacity. 2024-03-10 22:00:51 +08:00
ShikiSuen aa4162fa9b DataCompiler // Fix SQLite random segmentation fault 11. 2024-03-10 00:03:54 +08:00
ShikiSuen 951f41461a TDKCandidates // Refactor highlightedColor(). 2024-03-09 04:14:59 +08:00
ShikiSuen f46cfda6f5 [VersionUp] 3.8.4 GM Build 3840. 2024-03-08 03:21:39 +08:00
ShikiSuen 03edccff4f Update Data - 20240308 2024-03-08 03:18:05 +08:00
ShikiSuen d8aba434d9 SecureEventInputSputnik // Patch a memory leak, etc. 2024-03-08 01:54:11 +08:00
ShikiSuen 005116c429 SPM // Consolidate dependencies. 2024-03-06 00:18:55 +08:00
ShikiSuen 93256f0095 Xcode // Add a debuggable-only target. 2024-03-04 18:34:28 +08:00
ShikiSuen 11bb5a9b66 Xcode // Stop stripping Swift symbols. 2024-03-04 17:32:25 +08:00
ShikiSuen 9dc7821708 [VersionUp] 3.8.3 GM Build 3830. 2024-03-02 23:06:53 +08:00
ShikiSuen 5d62d5b66d Update Data - 20240301 2024-03-02 23:06:53 +08:00
ShikiSuen c63c531f1b MainAssembly // Include remaining AppDelegate IBOutlets. 2024-03-02 23:06:53 +08:00
ShikiSuen 35d4426730 UserPhrase // Improve score boosting / nerfing for single kanji. 2024-03-02 23:06:53 +08:00
ShikiSuen b628ddd082 LMInstantiator // Expose factoryCoreUnigramsFor(). 2024-03-02 23:06:53 +08:00
ShikiSuen 4e00791144 LMPlainBopomofo // Fix mistakes in Eten DOS CHS Sequence Data. 2024-03-02 23:06:53 +08:00
ShikiSuen 1e098cac53 InputHandler // Prioritize the handling of the service menu. 2024-03-02 23:06:53 +08:00
ShikiSuen 0107e7cd78 ServiceMenu // Filter some services if readings are unavailable. 2024-03-02 23:06:53 +08:00
ShikiSuen 9411686d03 UserDef // +useShiftQuestionToCallServiceMenu. 2024-03-02 23:06:53 +08:00
ShikiSuen 5eec7cd604 MainAssembly // + Candidate Service (Menu & Editor). 2024-03-02 23:06:53 +08:00
ShikiSuen dc79c629a1 CandidateNode // Subclass: ServiceMenuNode. 2024-03-02 23:06:53 +08:00
ShikiSuen 040c597345 Shared // +CandidateTextService. 2024-03-02 23:06:53 +08:00
ShikiSuen 923471c8bb UserDef // +kCandidateServiceMenuContents. 2024-03-02 23:06:53 +08:00
ShikiSuen 46d4e7bdb3 MainAssembly // Refactor wherever using UniformTypeIdentifiers. 2024-03-02 23:06:53 +08:00
ShikiSuen 55dcdc8ce0 (NS)String // Add some codepoint extensions. 2024-03-02 23:06:53 +08:00
ShikiSuen bd5fdcaa26 ClientListMgr // Fix metrics and the invisible scroller. 2024-03-02 23:06:53 +08:00
ShikiSuen d5d9167b1e CocoaImpl // Fix isDarkMode(). 2024-03-02 23:06:53 +08:00
ShikiSuen c2679735c1 PrefMgr // Refactor the didSet methods. 2024-03-02 23:06:53 +08:00
ShikiSuen 4904664277 Shared // Fix a KVO Observer. 2024-03-02 23:06:53 +08:00
ShikiSuen 76dd75ce5a UserDef // +kSpecifyCmdOptCtrlEnterBehavior. 2024-03-02 23:06:53 +08:00
ShikiSuen 7be2a85b25 BrailleSputnik // Initial Implementation. 2024-03-02 23:06:53 +08:00
ShikiSuen 549c361af4 [VersionUp] 3.8.2 GM Build 3820. 2024-03-02 23:06:53 +08:00
ShikiSuen 72655119fa Update Data - 20240223 2024-02-24 03:56:21 +08:00
ShikiSuen 3ebb5f2f48 LMAssembly // Integrate EtenDOS SCPC data into the codebase. 2024-02-23 14:01:30 +08:00
ShikiSuen e44843e603 LMAssembly // Pack LMUserOverride inside LMInstantiator, etc. 2024-02-23 13:55:29 +08:00
ShikiSuen c5899152e6 InputHandler // Move some case-switch results to InputMode enum. 2024-02-21 14:58:26 +08:00
ShikiSuen 275288ea61 Hotenka // Deprecate NSJSONSerialization. 2024-02-19 01:33:47 +08:00
ShikiSuen 4184c3c1d2 DataCompiler // Post-dump SQLite database. 2024-02-18 23:58:06 +08:00
ShikiSuen a559818111 [VersionUp] 3.8.1 SP1 Build 3811. 2024-02-17 17:18:41 +08:00
ShikiSuen 9e3f0d7929 SessionCtl // Fix incorrect menu behavior. 2024-02-17 17:18:36 +08:00
ShikiSuen 3545cc1c22 AppInstaller // Add missing copyright label. 2024-02-17 16:20:59 +08:00
ShikiSuen 69d839e833 VwrAboutCocoa // Patch vertical button stack spacing for macOS 10.9. 2024-02-17 16:20:59 +08:00
ShikiSuen e887ba01a5 CtlCandidateTDK // Patch another issue with reverse lookup. 2024-02-17 16:20:59 +08:00
ShikiSuen b20dfec630 TDKCandidates // Force-refresh reverse lookup results on refresh. 2024-02-17 12:41:45 +08:00
ShikiSuen 63f7cc91fc [VersionUp] 3.8.1 GM Build 3810. 2024-02-17 00:37:24 +08:00
ShikiSuen b9595bed4f Update Data - 20240217 2024-02-17 00:09:04 +08:00
ShikiSuen 83b80fb863 AboutCocoa & ClientListMgr // Tweak metrics. 2024-02-17 00:07:46 +08:00
ShikiSuen 791256cf31 SessionCtl // Optimize the IME menu for macOS 10.9. 2024-02-17 00:07:46 +08:00
ShikiSuen 0e4651e70e UserDef // +filterNonCNSReadingsForCHTInput. 2024-02-16 16:18:34 +08:00
ShikiSuen 58815d7c54 LMAssembly // Implement CNS pronunciation filter. 2024-02-16 16:18:34 +08:00
ShikiSuen b479acf779 Repo // Add KimoDataReader using NSConnection. 2024-02-16 16:18:34 +08:00
ShikiSuen 424a736c8e TDKCandidates // Support displaying codepoints. 2024-02-15 13:44:25 +08:00
ShikiSuen ccd9b391e4 UserDef // +showCodePointInCandidateUI. 2024-02-15 13:44:25 +08:00
ShikiSuen 5090c7e6d4 CocoaExtension // Allow overriding stack spacing. 2024-02-14 22:36:54 +08:00
ShikiSuen 169902db19 SettingsCocoa // Simplify some phrases. 2024-02-14 22:15:49 +08:00
ShikiSuen 3af51f22e1 SettingsCocoa // Share metrics across panes. 2024-02-14 22:15:49 +08:00
ShikiSuen ad98484094
GitHub // Update CI to build SPM packages instead. 2024-02-14 17:54:10 +08:00
ShikiSuen 982686018b Makefile // Optimize for SPM packages. 2024-02-14 17:41:34 +08:00
ShikiSuen b013dc4d82 FileOpenMethod // Use localized app names. 2024-02-14 14:37:47 +08:00
ShikiSuen bb4729ee3f LMMgr // Fix when to reload phrase editors. 2024-02-14 13:44:22 +08:00
ShikiSuen 71e34790e8 SettingsCocoa // Add file drag receiver button for Kimo Data import. 2024-02-14 04:32:49 +08:00
ShikiSuen 54f61a28b1 Repo // Unify modal window calling methods. 2024-02-14 04:14:17 +08:00
ShikiSuen 21dcb58748 SettingsCocoa // Make narration settings effective immediately. 2024-02-13 20:53:34 +08:00
ShikiSuen 903faae51f SettingsCocoa // Lock dimensions for descriptions and titles. 2024-02-13 18:10:38 +08:00
ShikiSuen 820ee5b0f6 CocoaImpl // Fix NSView.makeSimpleConstraint(). 2024-02-13 18:10:38 +08:00
ShikiSuen 2ce79e5a05 SettingsCocoa // Also layout subtree at final step. 2024-02-13 18:10:38 +08:00
ShikiSuen 92b2ada9c5 LatinKeyboardMappings // Maintenance fix with Dvorak.QwertyCMD. 2024-02-13 18:10:38 +08:00
ShikiSuen e749e65627 Xcode // FUCK OFF USER SCRIPT SANDBOXING. 2024-02-13 00:19:42 +08:00
ShikiSuen 1b3dcb0e0e [VersionUp] 3.8.0 SP1 Build 3801. 2024-02-13 00:10:44 +08:00
ShikiSuen fcb839bb33 PkgInstaller // Fix a wrong parameter in postflight script. 2024-02-13 00:10:44 +08:00
ShikiSuen 1111a249ec SettingsCocoa // Fix an issue with menu tags in PhraseEditor. 2024-02-13 00:08:21 +08:00
ShikiSuen 5d8680c4b5 Main // Support "--import-kimo" terminal parameter, etc. 2024-02-13 00:08:21 +08:00
ShikiSuen ae73869a2d [VersionUp] 3.8.0 GM Build 3800. 2024-02-12 15:30:27 +08:00
ShikiSuen 933fdb2347 Update Data - 20240212 2024-02-12 15:28:11 +08:00
ShikiSuen 026fbd7fa4 Repo // Update CNS data timestamp to 2024-01-23. 2024-02-12 15:20:09 +08:00
ShikiSuen 177d0c87a3 PCB // Add shadow for cursor. 2024-02-12 14:26:37 +08:00
ShikiSuen 89d07b2edd Repo // Checking J / K key validity as candidate keys. 2024-02-12 04:08:27 +08:00
ShikiSuen 072d39790e UserDef // + useJKtoMoveCompositorCursorInCandidateState. 2024-02-12 04:08:27 +08:00
ShikiSuen 319a1f8d4a Repo // Introducing LocalizableFileSorter script. 2024-02-12 04:08:27 +08:00
ShikiSuen e1b7a4df9f InputHandler // +Enum: TypingMethod. 2024-02-12 04:08:27 +08:00
ShikiSuen 28e53c27ad UserDef // + dodgeInvalidEdgeCandidateCursorPosition. 2024-02-12 04:08:27 +08:00
ShikiSuen 095a0a34c5 Localizable // Tweak file structure. 2024-02-12 04:08:27 +08:00
ShikiSuen b82467dc2d MainAssembly // Organize certain source code files. 2024-02-12 04:08:27 +08:00
ShikiSuen f4a407a860 Settings // Use unique toolbar identifiers. 2024-02-12 04:08:27 +08:00
ShikiSuen b8f5077198 PrefUITabs // Update new icons for macOS 10.x. 2024-02-12 04:08:27 +08:00
ShikiSuen 4af0d515dc SettingsCocoa // Center the toolbar icons. 2024-02-12 04:08:27 +08:00
ShikiSuen 7fd6ea36da UpdateSputnik // Patch a misnotification. 2024-02-12 04:08:27 +08:00
ShikiSuen df5075972a IMKHelper // Fix a TISInputSource installation crash in macOS 10.9. 2024-02-12 04:08:27 +08:00
ShikiSuen cfad082b14 NSEventImpl // Again fix NSInternalInconsistencyException issue. 2024-02-10 13:39:30 +08:00
ShikiSuen 965008fb7f TDKCandidates // Refactor context menu items. 2024-02-10 02:35:06 +08:00
ShikiSuen bb9bc058cc SessionCtl // Refactor the menu structure. 2024-02-10 02:35:05 +08:00
ShikiSuen a219b7881f AboutUI // Reimplement without XIB for old systems. 2024-02-10 02:35:05 +08:00
ShikiSuen 80fe5ecb74 Repo // Move RevLookUpWindow to MainAssembly. 2024-02-10 02:35:05 +08:00
ShikiSuen e071ece2f3 Localizable // Maintenance. 2024-02-10 02:35:05 +08:00
ShikiSuen 962e61f6f3 ClientListMgr // Reimplement without XIB. 2024-02-10 02:35:05 +08:00
ShikiSuen 8679943b9d Repo // Deprecating CtlPrefWindow. 2024-02-10 02:35:05 +08:00
ShikiSuen 2465814e55 SettingsCocoa // First implementation, replacing CtlPrefWindow. 2024-02-10 02:35:05 +08:00
ShikiSuen b8c915dca0 Repo // + UserDefRenderableCocoa & extending AppKit. 2024-02-10 02:33:58 +08:00
ShikiSuen 368f9bb653 SwiftImpl // Add array builder and Bool.from(integer:). 2024-02-09 18:37:20 +08:00
ShikiSuen a1d9f502c1 Repo // Implementing FileOpenMethod. 2024-02-09 18:37:20 +08:00
ShikiSuen c0e1eb449d PEUI // Patch textView.isRichText. 2024-02-09 01:37:34 +08:00
ShikiSuen e3a775dfa5 PEUI // Fix i18n. 2024-02-06 21:33:19 +08:00
ShikiSuen 2fb1b22270 SettingsUI // Update a binding & Folder structure changes. 2024-02-06 03:23:42 +08:00
ShikiSuen a448c4cf7a SwiftUI // Fix the wrong term "onChange" to "didChange". 2024-02-04 00:16:34 +08:00
ShikiSuen 77f39023ab [VersionUp] 3.7.3 GM Build 3730. 2024-02-02 20:08:41 +08:00
ShikiSuen 0905d5c50d Update Data - 20240202 2024-02-02 20:07:32 +08:00
ShikiSuen 85833c40d9 SecureEventInputSputnik // Also monitor hibernation status. 2024-01-30 14:13:51 +08:00
ShikiSuen 6b0353107f CheatSheet // Add new contents to reflect recent feature changes. 2024-01-30 13:28:47 +08:00
ShikiSuen 38fcbb3e46 Repo // Update CNS data timestamp to 2024-01-15. 2024-01-29 23:12:22 +08:00
ShikiSuen aac80eba7d InputHandler // Allow moving cursor in candidate state.
- The hotkey is (Shift+)Opt+FWD/BWD.
2024-01-29 22:23:35 +08:00
ShikiSuen 24cf7e9971 InputHandler // Improve precision of handling modified flags. 2024-01-29 21:19:15 +08:00
ShikiSuen 4c45f4ddda PrefWindow // + numPadCharInputBehavior. 2024-01-29 21:18:56 +08:00
ShikiSuen 56d4f42641 SettingsUI // + numPadCharInputBehavior. 2024-01-29 21:18:56 +08:00
ShikiSuen 782f3bd21c InputHandler // + handleNumPadKeyInput(). 2024-01-29 21:18:56 +08:00
ShikiSuen ffd64bd7a8 UserDef // + numPadCharInputBehavior. 2024-01-29 21:18:56 +08:00
ShikiSuen 6763cf7889 Localizable // Fix CJK colons. 2024-01-29 21:18:56 +08:00
ShikiSuen ca3a3abf9c CtlPrefWindow // i18n fixes. 2024-01-29 21:18:56 +08:00
ShikiSuen 01924a1cf1 SettingsUI // Massive refactor using UserDefRenderable. 2024-01-29 21:18:56 +08:00
ShikiSuen 09aec2bb06 SettingsUI // Implement UserDefRenderable. 2024-01-29 21:18:56 +08:00
ShikiSuen cfe9a1ce5d LMInstantiator // Add ability for supplying NumPad results. 2024-01-29 21:18:56 +08:00
ShikiSuen 586822c981 Repo // Refactor APIs related to LM access and configs. 2024-01-29 21:18:56 +08:00
ShikiSuen 23ef3124d4 Shared // Implementing KBEvent. 2024-01-29 21:18:56 +08:00
ShikiSuen 5e1208bc5e InputHandler // Implement (BOOL)handleCandidateInput:ignoringModifiers:. 2024-01-19 11:53:17 +08:00
ShikiSuen 71dc1afc48 ShiftKeyUpChecker // Change delay interval to 0.2. 2024-01-19 11:53:17 +08:00
ShikiSuen b3ae482c70 PCB // Renovate the UI theme. 2024-01-19 11:53:17 +08:00
ShikiSuen 68d61f2311 SecureEventInputSputnik // Patch the detection method for screen savers. 2024-01-09 09:57:49 +08:00
ShikiSuen 5fb0bab91a [VersionUp] 3.7.2 GM Build 3720. 2024-01-08 01:23:38 +08:00
ShikiSuen f638de4b35 Update Data - 20240108 2024-01-08 01:22:57 +08:00
ShikiSuen 68b72112db Xcode // Re-enable build-script sandboxing. 2024-01-07 18:59:28 +08:00
ShikiSuen ad5fd50733 DataCompiler // Set SQLite journal mode == OFF, etc. 2024-01-07 18:55:28 +08:00
ShikiSuen 53effaae0a Hotenka // Set SQLite journal mode == OFF. 2024-01-07 18:49:43 +08:00
ShikiSuen 86bab5c7a8 LMInstantiator // Set SQLite journal mode == OFF. 2024-01-07 18:49:43 +08:00
ShikiSuen 3cd327eb15 vChewingLM // Fix [String].runAsSQLPreparedSteps(). 2024-01-07 18:49:43 +08:00
ShikiSuen fe34bfffa0 PrefWindow // Fix an i18n usage of colons. 2024-01-07 18:49:37 +08:00
ShikiSuen ed93a87f47 PrefWindow // Bind "ReadingNarrationCoverage" option. 2024-01-07 18:49:37 +08:00
ShikiSuen edd38d8c92 SettingsUI // Bind "ReadingNarrationCoverage" option. 2024-01-07 18:49:37 +08:00
ShikiSuen c85e7142fc Repo // Implement "ReadingNarrationCoverage" feature. 2024-01-07 18:49:37 +08:00
ShikiSuen 2a694081bd UserDef & Prefs // Add "ReadingNarrationCoverage" option. 2024-01-07 18:49:37 +08:00
ShikiSuen 2ec3214491 Repo // Introducing associated phrases in non-SCPC mode.
- Our implementation doesn't use compositor to handle associated phrases, considering that there are too many polyphonic ideographs in Mandarin Chinese.
- This implementation is NOT meant to be as competitive as the similar feature in McBopomofo PR416 (which uses compositor but has issues with polyphonic ideographs).
- This also brings related updates for CheatSheet.
- The translated terms of "Associated Phrases" are changed in this commit.
2024-01-07 18:49:37 +08:00
ShikiSuen 3e4c564248 InfoPlist // Maintenance edits. 2024-01-07 18:49:37 +08:00
ShikiSuen 95f0ff5fd8 SecureEventInputSputnik // New method for checking a locked desktop. 2024-01-07 18:49:36 +08:00
ShikiSuen cfe8f8759c SessionCtl // Fix selectionRange() for inline composition buffers. 2024-01-07 18:49:36 +08:00
ShikiSuen 622744e961 GitHub // Update CI settings. 2024-01-07 18:49:36 +08:00
ShikiSuen d7fb717030 [VersionUp] 3.7.1 SP1 Build 3711. 2024-01-01 02:05:24 +08:00
ShikiSuen 8a5d2a5f6f SessionCtl // Revert some buggy refactors. 2024-01-01 02:04:33 +08:00
ShikiSuen b449edc2a8 [VersionUp] 3.7.1 GM Build 3710. 2024-01-01 00:10:28 +08:00
ShikiSuen 7d53740f88 Update Data - 20240101 2024-01-01 00:05:33 +08:00
ShikiSuen 00b5be3784 InfoPlist // Refactor. 2023-12-31 23:51:42 +08:00
ShikiSuen 73b0da3eb4 Repo // Stop previous session from interfering current palettes. 2023-12-31 23:51:42 +08:00
ShikiSuen 1c92ab8edf LMCassette // Refactor && Fix .clear(). 2023-12-31 23:51:42 +08:00
ShikiSuen 4317c9c653 LMAssembly // Add a test against LMInstantiator with given tokens. 2023-12-29 16:50:31 +08:00
ShikiSuen b0e237e08d [VersionUp] 3.7.0 GM Build 3700. 2023-12-28 22:12:05 +08:00
ShikiSuen b1bb8a6710 Update Data - 20231228 2023-12-28 22:11:22 +08:00
ShikiSuen cd28c5d44d Assets // Make icons look more concordant on macOS 14 Sonoma.
- There are still issues need to be solved by Apple.
2023-12-28 22:11:22 +08:00
ShikiSuen a845dae62b TDKCandidates // Use Courier New for selection keys on old OS. 2023-12-28 22:11:22 +08:00
ShikiSuen d8e72674d7 InputHandler // Add support for `%keys_to_directly_commit`. 2023-12-27 14:53:37 +08:00
ShikiSuen 51580ac2fb LMCassette // Add support for `%keys_to_directly_commit`. 2023-12-27 14:53:37 +08:00
ShikiSuen 2a4b01c234 Repo // Close all panels of the previous session. 2023-12-27 14:53:37 +08:00
ShikiSuen c5ce9199bd LMAssembly // Implementing InputToken support. 2023-12-21 22:53:33 +08:00
ShikiSuen 87f7328636 Megrez // Turn KeyValuePaired and Unigram into Structs again.
- This can solve the crash issues while being deduplicated in Swift-only method.
2023-12-20 11:10:14 +08:00
ShikiSuen 1dcdc21411 SwiftImpl // Patch RangeReplaceableCollection.deduplicated(). 2023-12-20 11:10:14 +08:00
ShikiSuen 1e0ec83dda Xcode // Disable build-script sandboxing for now.
- This motherfucking feature hinders dictionary compiler from working well, bugging it with SQLite segmentation faults.
2023-12-20 11:10:14 +08:00
ShikiSuen 7339d6e6e6 SecureEventInputSputnik // Handle screen-saver and lock-screen. 2023-12-20 11:10:14 +08:00
ShikiSuen 0742af02a9 Config // Update config backup, adding GitLink repository. 2023-12-08 00:43:56 +08:00
ShikiSuen 87e367a81c Git // Switch GitLab from GL-China to GL-Global. 2023-12-04 17:25:20 +08:00
ShikiSuen 70a1b7896f [VersionUp] 3.6.3 SP1 Build 3631. 2023-12-04 17:23:46 +08:00
ShikiSuen 7b4cfb554a Update Data - 20231204 2023-12-04 17:23:46 +08:00
ShikiSuen 464838b721 SettingsUI // Ensure the visibility of scroll bars when should. 2023-12-04 17:23:46 +08:00
ShikiSuen 19339d8195 DataCompiler // Avoid segmentation faults. 2023-12-04 16:55:04 +08:00
ShikiSuen 5ef9a5b012 LMA // Fix wrong results given by hasUnigramsFor() for cassette module. 2023-12-04 15:26:10 +08:00
ShikiSuen 57a49cd245 [VersionUp] 3.6.3 GM Build 3630. 2023-12-04 15:26:10 +08:00
ShikiSuen 815e56db20 Update Data - 20231202 2023-12-04 15:26:10 +08:00
ShikiSuen 36fcff6614 LMMgr // Fix logical errors with external factory dict path. 2023-12-04 15:26:10 +08:00
ShikiSuen 8a634a9abd i18n // Fix certain texts regarding "plist". 2023-12-02 14:48:11 +08:00
ShikiSuen 3a748b6cc2 Xcode // Simplify the allowed output file list for Script Sandboxing. 2023-12-02 13:24:55 +08:00
ShikiSuen a8e72c902b Main // Allow dumping UserDefaults through terminal. 2023-12-02 13:22:41 +08:00
ShikiSuen 82dea233b6 PrefMgr // Add ability to dump pref data as a shell script. 2023-12-02 13:22:41 +08:00
ShikiSuen 238845fb1d UserDef // Let kClientsIMKTextInputIncapable dumpable. 2023-12-02 13:22:41 +08:00
ShikiSuen 33bcbff3dc DataCompiler // Only compile JSON when `--json` argument is given. 2023-12-02 13:22:41 +08:00
ShikiSuen a4e301beef DataCompiler // Correctly handle statement pointers. 2023-12-02 13:22:37 +08:00
ShikiSuen bbe1b409c5 LMAssembly // Faster query speed to check data existence. 2023-12-02 13:22:37 +08:00
ShikiSuen 161aa100cc LMAssembly // Correctly handle statement pointers. 2023-12-02 13:22:37 +08:00
ShikiSuen a66879f7bf Hotenka // Patch a memory leak. 2023-12-02 00:53:15 +08:00
ShikiSuen af8b15e170 i18n // Fix the description for .RespectClientAccentColor(). 2023-12-02 00:53:15 +08:00
ShikiSuen c45772b90c SessionCtl // Don't read client accent if the OS accent is customized. 2023-12-02 00:53:15 +08:00
ShikiSuen 274534b0f2 CocoaImpl // Check whether "AppleAccentColor" is specified. 2023-12-02 00:53:15 +08:00
ShikiSuen 2f81bd5824 Megrez // Add a safe check in Compositor.update(). 2023-12-02 00:53:15 +08:00
ShikiSuen 2880901491 BookmarkManager // Better compatibility with URL bookmarks. 2023-12-02 00:49:41 +08:00
ShikiSuen de1272f389 [VersionUp] 3.6.2 GM Build 3620. 2023-11-29 22:19:21 +08:00
ShikiSuen f95eac4ab8 Update Data - 20231129 2023-11-29 22:19:21 +08:00
ShikiSuen 1c435aefc6 Repo // Update CNS version timestamp to v2023-11-06. 2023-11-29 22:19:21 +08:00
ShikiSuen bee7b6e555 InfoPlist // Update TISIconLabels. 2023-11-29 22:16:56 +08:00
ShikiSuen 344b08c7a2 AboutUI // Fine-tune the visual style. 2023-11-29 11:24:28 +08:00
ShikiSuen 4b0ec368e7 AppInstaller // Fine-tune the visual style. 2023-11-29 11:24:28 +08:00
ShikiSuen 4c97eae53e i18n // Term fix. 2023-11-29 11:24:28 +08:00
ShikiSuen 67a2734d44 AppDelegate // Set the maximum allowed memory usage to 384MB. 2023-11-29 09:24:47 +08:00
ShikiSuen eb68454d8d SessionCtl // Show memory usage when debug mode is ON. 2023-11-29 09:24:47 +08:00
ShikiSuen 334e6e0ad5 PrefUI // Bind respectClientAccentColor(). 2023-11-29 09:24:47 +08:00
ShikiSuen d30013a552 PrefWindow // Bind respectClientAccentColor(). 2023-11-29 09:24:47 +08:00
ShikiSuen 9b1fdb9c69 TDKCandidates // Read delegate.clientAccentColor(). 2023-11-29 09:24:47 +08:00
ShikiSuen 8c70327d90 SessionCtl // Add .clientAccentColor(). 2023-11-29 09:24:47 +08:00
ShikiSuen 61dc2b991f UserDefs // +respectClientAccentColor(). 2023-11-29 09:24:47 +08:00
ShikiSuen 4d623c58a4 CocoaImpl // Add API for finding accent colors. 2023-11-29 09:24:47 +08:00
ShikiSuen e5591ef9cc CtlPrefWindow // Compatibility with vintage macOS versions. 2023-11-27 23:54:39 +08:00
ShikiSuen 133901ede2 Repo // Add SQLite support for factory database. 2023-11-27 23:54:39 +08:00
ShikiSuen 40d866714e TDKCandidates // Fix a bug with single-page matrix pools. 2023-11-27 19:06:18 +08:00
ShikiSuen 787bbd5c0f TDKCandidates // Patch the bgColor of the current candidate line. 2023-11-27 19:06:18 +08:00
ShikiSuen 93da928342 AppInstaller // i18n fix. 2023-11-18 00:15:04 +08:00
ShikiSuen ee82abc3dd AboutUI // Make close button effective on macOS 13. 2023-11-09 22:20:42 +08:00
ShikiSuen 728570a342 AppInstaller // Compatibility with Xcode 14.2. 2023-11-05 22:50:46 +08:00
ShikiSuen d2f4fe6f2f [VersionUp] 3.6.1 SP2 Build 3612. 2023-11-01 21:40:05 +08:00
ShikiSuen ad2d4fd781 PrefWindow // Add a button for importing KeyKey user data. 2023-11-01 21:40:05 +08:00
ShikiSuen f26025ffe7 PrefUI // Add a button for importing KeyKey user data. 2023-11-01 21:40:01 +08:00
ShikiSuen b0c5ba0f98 LMMgr // + importYahooKeyKeyUserDictionary(). 2023-11-01 21:03:33 +08:00
ShikiSuen d5117dc3e5 i18n // Prepare for the next new feature. 2023-11-01 20:36:24 +08:00
ShikiSuen 6417c0193e CapsLockToggler // Fix a memory-leak issue. 2023-11-01 13:46:01 +08:00
ShikiSuen 3fbb0b418f LMMgr // Fix openPhraseFile().
- This is only needed in mainstream releases.
2023-11-01 11:38:29 +08:00
ShikiSuen f9f0c21b9e [VersionUp] 3.6.1 SP1 Build 3611. 2023-10-30 12:01:48 +08:00
ShikiSuen 36fe4fced3 SessionCtl // Deprecate .isCapsLocked(). 2023-10-30 12:01:48 +08:00
ShikiSuen 65cf83d902 Repo // Fix the capslock state detection. 2023-10-30 11:01:36 +08:00
ShikiSuen c943f4ec52 SessionCtl // Remove PreCommit Handling. 2023-10-30 10:46:21 +08:00
ShikiSuen 378e32d7cd README // Update system requirements. 2023-10-29 01:39:39 +08:00
ShikiSuen 2e52a6b2d8 [VersionUp] 3.6.1 GM Build 3610. 2023-10-28 22:47:56 +08:00
ShikiSuen eb5a867a37 Update Data - 20231028 2023-10-28 22:47:56 +08:00
ShikiSuen 1b73504572 SessionCtl // Hide notifications if dedicated CpLk processing is off. 2023-10-28 22:47:56 +08:00
ShikiSuen dca874dc2a NSEvent // Omit .capslock from .keyModifierFlags(). 2023-10-28 22:47:56 +08:00
ShikiSuen 64837d699a InputHandler // Fix Zhuyin typing in CapsLock mode. 2023-10-28 22:47:56 +08:00
ShikiSuen c55c54c7e8 PkgInstaller // Patch some commands to let them always return 0. 2023-10-28 22:47:56 +08:00
ShikiSuen d66d9799c0 SessionCtl // Clear ICB display before committing things. 2023-10-28 22:47:56 +08:00
ShikiSuen 2a5cc07b5b NotifierUI // Compensate the window area with system notifications. 2023-10-28 22:47:56 +08:00
ShikiSuen ffe943dfbc PrefUI // Add two new UserDef items. 2023-10-28 22:47:56 +08:00
ShikiSuen 9c525b1d0e PrefWindow // Add two new UserDef items. 2023-10-28 22:47:56 +08:00
ShikiSuen 54c1e60d7e MainAssembly // Bind two new UserDef items. 2023-10-28 22:47:56 +08:00
ShikiSuen 1f9a9fa0c0 Prefs // Add two new UserDef items. 2023-10-28 22:47:56 +08:00
ShikiSuen ac653015c6 UserDef // Add 2 new keys regarding Alphanumerical Modes. 2023-10-28 22:47:56 +08:00
ShikiSuen 608d8970bd Repo // Remove PreferencePane.
- It became ineffective since macOS 10.15 Catalina.
2023-10-28 22:47:56 +08:00
ShikiSuen cbdc68e180 Repo // Notice that Shift-key toggle only works when CpLk is OFF. 2023-10-28 22:47:56 +08:00
ShikiSuen 7f744589af PrefWindow // +checkAbusersOfSecureEventInputAPI(). 2023-10-28 22:47:56 +08:00
ShikiSuen 6e946f5db1 PrefUI // +checkAbusersOfSecureEventInputAPI(). 2023-10-28 22:47:56 +08:00
ShikiSuen 8d89da0c2b Prefs // +checkAbusersOfSecureEventInputAPI(). 2023-10-28 22:47:56 +08:00
ShikiSuen 9a0d0dc633 Repo // Introduce SecurityAgentHelper. 2023-10-28 22:47:56 +08:00
ShikiSuen 5460217594 CocoaExtension // Add SecureEventInputSputnik. 2023-10-28 22:47:56 +08:00
ShikiSuen 90d1803cf3 SessionCtl // Turn off isASCIIMode if CapsLock is turned off. 2023-10-28 22:47:56 +08:00
ShikiSuen 6be6492470 SessionCtl // Turn off Caps Lock if Eisu key is turned off. 2023-10-28 22:47:56 +08:00
ShikiSuen 6c371b844f Repo // Add HangarRash_SwiftyCapsLockToggler. 2023-10-28 22:47:56 +08:00
ShikiSuen b7c914a611 NSEvent // Use filtered modifier flags. 2023-10-28 22:47:56 +08:00
ShikiSuen acce63d7bc SymbolMenu // Add subset for radicals. 2023-10-28 22:47:56 +08:00
ShikiSuen efe2abb03d TDKCandidates // Tweak default background color. 2023-10-28 22:47:56 +08:00
ShikiSuen ade9953756 MainAssembly // Remove PrefMgrObservable. 2023-10-28 22:47:56 +08:00
ShikiSuen ac4ed0a320 Repo // Add SCPC sequence data from Eten DOS. 2023-10-28 22:47:56 +08:00
ShikiSuen 7bab872346 Assets // +AppIconFallback. 2023-10-28 22:47:56 +08:00
ShikiSuen 1025dca837 CtlAboutUI // Refactor using SwiftUI. 2023-10-28 22:47:56 +08:00
ShikiSuen d18eb5d45b SettingsUI // Remove an unused variable. 2023-10-28 22:47:56 +08:00
ShikiSuen fac382a4ea AppInstaller // Rewrite using SwiftUI. 2023-10-28 22:47:56 +08:00
ShikiSuen 6c2a3ee8b4 PhraseEditorUI // Fine-tweaks. 2023-10-28 22:47:56 +08:00
ShikiSuen 5b11ffec44 SwiftPackages // Boost platform req. to macOS 11. 2023-10-28 22:47:56 +08:00
ShikiSuen 293582fa5d Repo // Boost minimum OS req. to macOS 12. 2023-10-28 22:47:56 +08:00
ShikiSuen bc94bb8192 [VersionUp] 3.6.0 GM Build 3600. 2023-09-28 07:52:30 -05:00
ShikiSuen d724016cb4 Update Data - 20230928 2023-09-28 07:52:30 -05:00
ShikiSuen 844c259aa6 Repo // Putting SettingsUI into MainAssembly. 2023-09-28 07:52:30 -05:00
ShikiSuen 3de134d640 Repo // Deprecating `SwiftUIBackports`. 2023-09-28 07:52:30 -05:00
ShikiSuen fa979137df PrefUI // Boost SwiftUI requirements to macOS 13. 2023-09-28 07:52:30 -05:00
ShikiSuen f81f2d3b43 PrefUI // Massive renovation - phase 2.
- This is the last workable commit prior to boosting the SwiftUI system requirements from macOS 10.15 Catalina to macOS 13 Ventura.
2023-09-28 07:52:30 -05:00
ShikiSuen 37104cb8a9 Repo // Deprecating Sindresorhus's Preferences package. 2023-09-28 07:52:30 -05:00
ShikiSuen 109ec7382d PrefUI // Massive renovation - phase 1.
- This is the last workable commit prior to the deprecation of the SSPreferences package.
2023-09-28 07:52:30 -05:00
ShikiSuen 4583bcc562 PathControl // Fix constraint behavior in SwiftUI. 2023-09-28 07:52:30 -05:00
ShikiSuen 00da1266f1 UpdateSputnik // Implement cross-distro upgrade. 2023-09-28 07:52:30 -05:00
ShikiSuen deee89e575 CtlPrefWindow // Add tooltip for navigation items. 2023-09-28 07:52:30 -05:00
ShikiSuen 711767df93 NotifierUI // Share changes across releases. 2023-09-28 07:52:30 -05:00
ShikiSuen 6b544c571d UpdateSputnik // Change macOS version threshold. 2023-09-28 07:52:30 -05:00
ShikiSuen b9552a4c91 UpdateInfo // Add links for the legacy branch. 2023-09-28 07:52:30 -05:00
ShikiSuen 96d86f6b02 PrefMgr // Add observability. 2023-09-28 07:52:30 -05:00
ShikiSuen 02a059a629 AppProperty // Implement `DynamicProperty` protocol. 2023-09-28 07:52:30 -05:00
ShikiSuen b4a669650a Localizable // Grammar fix, etc. 2023-09-28 07:52:30 -05:00
ShikiSuen e5363efa03 PrefWindow // Add toggle for CapsLock notifications. 2023-09-28 07:52:30 -05:00
ShikiSuen 5a1826c3bd SessionCtl_Menu // Hide some items if serving `com.apple.SecurityAgent`. 2023-09-28 07:52:30 -05:00
ShikiSuen f1cfe67c97 Repo // Don't check update if serving `com.apple.SecurityAgent`. 2023-09-28 07:52:30 -05:00
ShikiSuen 95b8f3fd4c SessionCtl // Sync some syntax preferences from vC-Aqua branch. 2023-09-28 07:52:30 -05:00
ShikiSuen eb6c747272 PrefUI // +.securityHardenedCompositionBuffer(). 2023-09-28 07:52:30 -05:00
ShikiSuen 28ebe504c2 PrefWindow // +.securityHardenedCompositionBuffer(). 2023-09-28 07:52:30 -05:00
ShikiSuen e915cd4503 SessionCtl // Bind .securityHardenedCompositionBuffer(). 2023-09-28 07:52:30 -05:00
ShikiSuen e7b59a8cab Prefs // +.securityHardenedCompositionBuffer(). 2023-09-28 07:52:30 -05:00
ShikiSuen 3b20d05ea9 TooltipUI // Maintenance. 2023-09-28 07:52:30 -05:00
ShikiSuen 6b2a82e2f8 PrefWindow // Add options for handling Shift-key toggles. 2023-09-28 07:52:30 -05:00
ShikiSuen 0b9256927e Prefs // Trim .enableSwiftUIForTDKCandidates(). 2023-09-28 07:52:30 -05:00
ShikiSuen cf51277625 PCB // Fix errors in manipulating text attributes. 2023-09-28 07:52:30 -05:00
ShikiSuen 62742e9f82 PrefUI // Trim settings for toggling SwiftUI in TDKCandidates. 2023-09-28 07:52:30 -05:00
ShikiSuen 4eacbf6f8c TDKCandidates // Removing SwiftUI support for now.
- SwiftUI is not suitable for necessities like writing a performance-critical candidate window.
2023-09-28 07:52:30 -05:00
ShikiSuen 1e077faf03 InfoPlist // Remove TISDoubleSpaceSubstitution. 2023-09-28 07:52:30 -05:00
ShikiSuen c323a6efe8 GitHub // Force Xcode 15.x in Github CI. 2023-09-28 07:52:30 -05:00
ShikiSuen f9b3eb6cd4 IMKHelper // Fix an error while freeing a pointer. 2023-09-28 07:52:30 -05:00
ShikiSuen db4ccbb705 NSAttributedTextView // Compatibility tweaks. 2023-09-28 07:52:30 -05:00
ShikiSuen 90c35db4d1 Repo // Xcode 14.2 comaptibility. 2023-09-28 07:52:30 -05:00
ShikiSuen e6e27ee937 Repo // Use Xcode 15 recommended settings. 2023-09-28 07:52:30 -05:00
ShikiSuen 7773441f09 Repo // Deprecating Theme Color. 2023-09-28 07:52:30 -05:00
ShikiSuen 804164740e TDKCandidates // Patch `.kern` for macOS 14. 2023-09-28 07:52:30 -05:00
ShikiSuen 53e319d913 Preferences // Use ObjC instead. 2023-09-28 07:52:30 -05:00
ShikiSuen d7d6a584e0 [VersionUp] 3.5.5 GM Build 3505. 2023-09-15 19:39:23 +08:00
ShikiSuen 6a4b001449 Update Data - 20230915 2023-09-15 19:39:23 +08:00
ShikiSuen 27ccac62d2 SessionCtl // Do something with non-JIS keyboard on activation. 2023-09-15 19:39:23 +08:00
ShikiSuen 153572b826 IMEApp // Implement .isKeyboardJIS(). 2023-09-15 19:39:23 +08:00
ShikiSuen 63422c9841 SessionCtl // Remove a redundant statement. 2023-09-15 19:39:23 +08:00
ShikiSuen ee97c5a5f1 ShiftKeyUpChecker // Enforce keyCode equality between contextual events. 2023-09-15 19:39:23 +08:00
ShikiSuen 098a10dff7 NSEvent // Simplifying ModifierFlags.contains(). 2023-09-15 19:39:23 +08:00
ShikiSuen f7bff78c4a Repo // Migrate a condition regarding isASCIIMode. 2023-09-15 19:39:23 +08:00
ShikiSuen 879243005f SessionCtl // Remove a JIS-Incompatible condition. 2023-09-15 19:39:23 +08:00
ShikiSuen b428f18d60 Xcode // Update debug settings. 2023-09-15 19:39:23 +08:00
ShikiSuen 6aa74c62b8 AppDelegate // Boost memory check maximum value to 768MB. 2023-09-15 19:39:23 +08:00
ShikiSuen a02613a28a SessionCtl // Only setMarkedText() if newValue != oldValue. 2023-09-15 19:39:23 +08:00
ShikiSuen c3228acd93 PrefMgr // Implementing Observability. 2023-09-15 19:39:23 +08:00
ShikiSuen 8b5e743b9f SessionCtl // Remove a duplicated command. 2023-09-15 19:39:23 +08:00
ShikiSuen 067f32e102 PrefWindow // Tweak .shareAlphanumericalModeStatusAcrossClients(). 2023-09-15 19:39:23 +08:00
ShikiSuen 21b22b02e8 PrefUI // Tweak .shareAlphanumericalModeStatusAcrossClients(). 2023-09-15 19:39:23 +08:00
ShikiSuen 0502be50b9 Localizable // Fix certain translations. 2023-09-15 19:39:23 +08:00
ShikiSuen 9cbf087d95 MainAssembly // Reload filter when necessary. 2023-09-15 19:39:23 +08:00
ShikiSuen 60b9f28fa3 CheatSheet // A typo fix in Japanese localization. 2023-09-15 19:39:23 +08:00
ShikiSuen 35dcd92f4f InputHandler // Update tooltip text for unfiltering phrases. 2023-09-15 19:39:23 +08:00
ShikiSuen e8ea97b71c IMEState // Expose .markedTargetIsCurrentlyFiltered(). 2023-09-15 19:39:23 +08:00
ShikiSuen 20e8650eab IMEStateData // Update tooltip if currently marked pair is filtered. 2023-09-15 19:39:23 +08:00
ShikiSuen e624e5a5dc TooltipUI // Add .information color scheme. 2023-09-15 19:39:23 +08:00
ShikiSuen 5cf913877c LMMgr // Add .checkIfPhrasePairIsFiltered(). 2023-09-15 19:39:23 +08:00
ShikiSuen 919278d9cf LMMgr // Simplify switch conditions with input modes, plus bug fix. 2023-09-15 19:39:23 +08:00
ShikiSuen 0f2ad53481 LMInstantiator & UserPhrase // New method to check isFiltered(). 2023-09-15 19:39:23 +08:00
ShikiSuen ee11682edb UserPhrases // Implement .removeFromFilter() method, etc. 2023-09-15 19:39:23 +08:00
ShikiSuen 1cc2929d95 Repo // Implement UserPhrase.isAlreadyFiltered(). 2023-09-15 19:39:23 +08:00
ShikiSuen 9c81700e98 IMEStateData // Optimize tooltip texts for phrase replacement mode. 2023-09-15 19:39:23 +08:00
ShikiSuen 4e7c4133ab PCB // Auto-close out-of-date window instances. 2023-09-15 19:39:23 +08:00
ShikiSuen ff2df510d7 TooltipUI // Auto-close out-of-date window instances. 2023-09-15 19:39:23 +08:00
ShikiSuen e22941e8ae CtlCandidateTDK // Auto-close out-of-date window instances. 2023-09-15 19:39:23 +08:00
ShikiSuen 09e5546e9b Repo // Ensure [weak self] wherever necessary. 2023-09-15 19:39:23 +08:00
ShikiSuen f4e4c37e60 Shared // Extend UserDef with necessary sub-properties. 2023-09-15 19:39:23 +08:00
ShikiSuen 66b63db64b i18n // Fix mistakes and trim outdated entries. 2023-09-15 19:39:23 +08:00
ShikiSuen 75cc4aad53 NSAttributedString // Hardcode default kern value in macOS 14. 2023-09-15 19:39:23 +08:00
ShikiSuen c5de30a3c3 PrefMgr // Unleash certain settings for macOS 13 and 14. 2023-09-15 19:39:23 +08:00
ShikiSuen 368cf6bdd0 UserDef // Fix a key name in UserDefaults. 2023-09-15 19:39:23 +08:00
ShikiSuen 80f9b9bb46 LMConsolidator // Optimize fixEOF(). 2023-09-15 19:39:23 +08:00
ShikiSuen 145fa34ebd UserPhrase // Remove forced post-write file consolidation. 2023-09-15 19:39:23 +08:00
ShikiSuen 480b362bf4 LMCassette // Supply generated %quick results when needed.
* Partial results are only supplied if  the line `%flag_disp_partial_match` presents in the cassette.
2023-09-15 19:39:23 +08:00
ShikiSuen e19aa9e83a PrefUI // Bind alwaysExpandCandidateWindow(). 2023-09-15 19:39:23 +08:00
ShikiSuen cc6c8bcf29 PrefWindow // Bind alwaysExpandCandidateWindow(). 2023-09-15 19:39:23 +08:00
ShikiSuen 755026ded6 SessionCtl // Bind alwaysExpandCandidateWindow(). 2023-09-15 19:39:23 +08:00
ShikiSuen 7af6eb7cfb UserDefaults // +alwaysExpandCandidateWindow(). 2023-09-15 19:39:23 +08:00
ShikiSuen f295471c79 PrefUI // Remove unnecessary linebreaks. 2023-09-15 19:39:23 +08:00
ShikiSuen 5ec86408eb PrefUI // Bind useDynamicCandidateWindowOrigin(). 2023-09-15 19:39:23 +08:00
ShikiSuen e5ad493988 PrefWindow // Bind useDynamicCandidateWindowOrigin(). 2023-09-15 19:39:23 +08:00
ShikiSuen 9e26086133 SessionCtl // Make dynamic candidate window origin togglable. 2023-09-15 19:39:23 +08:00
ShikiSuen 29bc2d74cc PrefMgr // Let all properties observable by KVO. 2023-09-15 19:39:23 +08:00
ShikiSuen 17b0418dae Prefs // +useDynamicCandidateWindowOrigin(). 2023-09-15 19:39:23 +08:00
ShikiSuen 824e2cfce0 UserDefaults // Fix a typo in a variable name. 2023-09-15 19:39:23 +08:00
ShikiSuen 2e9412819d [VersionUp] 3.5.4 GM Build 3504. 2023-08-29 14:48:09 +08:00
ShikiSuen 35424efff7 Update Data - 20230829 2023-08-29 14:48:09 +08:00
ShikiSuen 136e8088be MainAssembly // Implementing Unit Tests.
* Further unit tests will be implemented later according to other necessities.
2023-08-29 14:48:09 +08:00
ShikiSuen f81e6837b8 LMAssembly // Updating Unit Tests. 2023-08-29 14:48:09 +08:00
ShikiSuen 4224509dae CtlCandidateTDK // Auto-update tooltips when expanded. 2023-08-29 14:48:09 +08:00
ShikiSuen 2c132a4f92 Repo // Auto-expand candidate UI under certain conditions. 2023-08-29 14:48:09 +08:00
ShikiSuen 3d98e6d974 AppDelegate // Shift certain boot tasks for macOS 14 compatibility. 2023-08-29 14:48:09 +08:00
ShikiSuen eb52d2e1ac UOM // Fix an index-out-of-range issue. 2023-08-29 14:48:09 +08:00
ShikiSuen bddbca6413 UOM // Fix an issue which generates wrecked trigram keys. 2023-08-29 14:48:09 +08:00
ShikiSuen 16750e4bde SessionCtl // Add one more replacement attempt to the final commit. 2023-08-29 14:48:09 +08:00
ShikiSuen 3a8060bf88 Repo // Make UserDefaults unit-testable. 2023-08-29 14:48:09 +08:00
ShikiSuen c5c99894a9 SessionCtl // Check inputHandler's LM instead in handle(). 2023-08-29 14:48:09 +08:00
ShikiSuen c95b5cbda7 LMCoreJSON // Publicize certain methods. 2023-08-29 14:48:09 +08:00
ShikiSuen 8c8cfc47f5 SessionCtl // Fix handleKeyUp(). 2023-08-29 14:48:09 +08:00
ShikiSuen 2e3f08c4ff LMA // LMCoreNS -> LMCoreJSON. 2023-08-29 14:48:09 +08:00
ShikiSuen e76e4da01d Repo // Bundlize main components into a dedicated Swift package. 2023-08-29 14:48:09 +08:00
ShikiSuen 2cdeae1446 SPM // Preparations for the next development phrase. 2023-08-29 14:48:09 +08:00
ShikiSuen cd33a21587 Repo // Introducing Broadcaster for KVO operations. 2023-08-29 14:48:09 +08:00
ShikiSuen a56b55125a AppDelegate // Manage Update Info URL internally. 2023-08-29 14:48:09 +08:00
ShikiSuen 0142258f34 PrefUI // Explain why IMKCandidates gets deprecated. 2023-08-29 14:48:09 +08:00
ShikiSuen 006192db87 Repo // Deprecating IMKCandidates.
* This is the only one big obstacle that hinders vChewing from being migratable as a cross-platform (macOS & iOS) app. Plus, IMKCandidates is buggy. It is not likely to be completely fixed by Apple, and its devs are not allowed to talk about it to non-Apple individuals. That's why it is enough.
2023-08-29 14:48:09 +08:00
ShikiSuen 2ea94c8886 AppDelegate // Always relocate wrecked UOM data. 2023-08-29 14:48:09 +08:00
ShikiSuen b375da5c95 InputHandler // Properly handle arrow keys for certain submodes. 2023-08-29 14:48:09 +08:00
ShikiSuen 4a476e0a50 InputHandler // Fix isConsideredEmptyForNow(). 2023-08-29 14:48:09 +08:00
ShikiSuen f3673fa4a8 NSEvent // Implement keyModifierFlags, etc. 2023-08-29 14:48:09 +08:00
ShikiSuen 308e068dfc InputHandler // Turn off HaninKeyboardSymbolMode on clear(). 2023-08-29 14:48:09 +08:00
ShikiSuen 48978f396b InputHandler // Update comments. 2023-08-29 14:48:09 +08:00
ShikiSuen b2ee0e3972 TDKCandidates // Implement page-expansion feature. 2023-08-29 14:48:09 +08:00
ShikiSuen 2abb86f4b8 ShiftKeyUpChecker // Remove redundant contents. 2023-08-29 14:48:09 +08:00
442 changed files with 27950 additions and 32009 deletions

View File

@ -18,14 +18,14 @@
[branch "upd/dev"]
remote = origin
merge = refs/heads/upd/dev
[branch "bleed/1.5.x"]
remote = origin
merge = refs/heads/bleed/1.5.x
[remote "gitlink"]
url = https://gitlink.org.cn/vChewing/vChewing-macOS.git
fetch = +refs/heads/*:refs/remotes/gitlink/*
[remote "gitcode"]
url = https://gitcode.net/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/gitcode/*
[remote "gitlab"]
url = https://jihulab.com/vChewing/vChewing-macOS.git/
url = https://gitlab.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/gitlab/*
[remote "github"]
url = https://github.com/vChewing/vChewing-macOS/
@ -34,6 +34,7 @@
url = https://gitee.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/all/*
pushurl = https://gitee.com/vchewing/vChewing-macOS.git/
pushurl = https://gitlink.org.cn/vChewing/vChewing-macOS.git/
pushurl = https://gitcode.net/vChewing/vChewing-macOS.git/
pushurl = https://jihulab.com/vChewing/vChewing-macOS.git/
pushurl = https://gitlab.com/vChewing/vChewing-macOS.git/
pushurl = https://github.com/vChewing/vChewing-macOS/

View File

@ -1,4 +1,4 @@
name: Build-with-macOS-latest
name: debug-macOS-MainAssembly
on:
push:
branches: [ "main" ]
@ -7,16 +7,16 @@ on:
jobs:
build:
name: Build (latest)
runs-on: macOS-latest
name: Build
runs-on: macOS-13
env:
GIT_SSL_NO_VERIFY: true
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
xcode-version: '^15.1'
- uses: actions/checkout@v1
- name: Clean
run: make clean
run: make spmClean
- name: Build
run: git pull --all && git submodule sync; make update; make
run: make spmDebug

View File

@ -9,6 +9,7 @@
// requirements defined in MIT License.
import Foundation
import SQLite3
// MARK: -
@ -27,6 +28,14 @@ fileprivate extension String {
}
}
// MARK: - String as SQL Command
fileprivate extension String {
@discardableResult func runAsSQLExec(dbPointer ptrDB: inout OpaquePointer?) -> Bool {
ptrDB != nil && sqlite3_exec(ptrDB, self, nil, nil, nil) == SQLITE_OK
}
}
// MARK: - StringView Ranges Extension (by Isaac Xen)
fileprivate extension String {
@ -117,40 +126,125 @@ func cnvPhonabetToASCII(_ incoming: String) -> String {
private let urlCurrentFolder = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
private let urlCHSRoot: String = "./components/chs/"
private let urlCHTRoot: String = "./components/cht/"
private let urlCHSRoot: String = "\(urlCurrentFolder.path)/components/chs/"
private let urlCHTRoot: String = "\(urlCurrentFolder.path)/components/cht/"
private let urlKanjiCore: String = "./components/common/char-kanji-core.txt"
private let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt"
private let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt"
private let urlKanjiCore: String = "\(urlCurrentFolder.path)/components/common/char-kanji-core.txt"
private let urlMiscBPMF: String = "\(urlCurrentFolder.path)/components/common/char-misc-bpmf.txt"
private let urlMiscNonKanji: String = "\(urlCurrentFolder.path)/components/common/char-misc-nonkanji.txt"
private let urlPunctuation: String = "./components/common/data-punctuations.txt"
private let urlSymbols: String = "./components/common/data-symbols.txt"
private let urlZhuyinwen: String = "./components/common/data-zhuyinwen.txt"
private let urlCNS: String = "./components/common/char-kanji-cns.txt"
private let urlPunctuation: String = "\(urlCurrentFolder.path)/components/common/data-punctuations.txt"
private let urlSymbols: String = "\(urlCurrentFolder.path)/components/common/data-symbols.txt"
private let urlZhuyinwen: String = "\(urlCurrentFolder.path)/components/common/data-zhuyinwen.txt"
private let urlCNS: String = "\(urlCurrentFolder.path)/components/common/char-kanji-cns.txt"
private let urlOutputCHS: String = "./data-chs.txt"
private let urlOutputCHT: String = "./data-cht.txt"
private let urlOutputCHS: String = "\(urlCurrentFolder.path)/data-chs.txt"
private let urlOutputCHT: String = "\(urlCurrentFolder.path)/data-cht.txt"
private let urlJSONSymbols: String = "./data-symbols.json"
private let urlJSONZhuyinwen: String = "./data-zhuyinwen.json"
private let urlJSONCNS: String = "./data-cns.json"
private let urlJSONSymbols: String = "\(urlCurrentFolder.path)/data-symbols.json"
private let urlJSONZhuyinwen: String = "\(urlCurrentFolder.path)/data-zhuyinwen.json"
private let urlJSONCNS: String = "\(urlCurrentFolder.path)/data-cns.json"
private let urlJSONCHS: String = "./data-chs.json"
private let urlJSONCHT: String = "./data-cht.json"
private let urlJSONBPMFReverseLookup: String = "./data-bpmf-reverse-lookup.json"
private let urlJSONBPMFReverseLookupCNS1: String = "./data-bpmf-reverse-lookup-CNS1.json"
private let urlJSONBPMFReverseLookupCNS2: String = "./data-bpmf-reverse-lookup-CNS2.json"
private let urlJSONBPMFReverseLookupCNS3: String = "./data-bpmf-reverse-lookup-CNS3.json"
private let urlJSONBPMFReverseLookupCNS4: String = "./data-bpmf-reverse-lookup-CNS4.json"
private let urlJSONBPMFReverseLookupCNS5: String = "./data-bpmf-reverse-lookup-CNS5.json"
private let urlJSONBPMFReverseLookupCNS6: String = "./data-bpmf-reverse-lookup-CNS6.json"
private let urlJSONCHS: String = "\(urlCurrentFolder.path)/data-chs.json"
private let urlJSONCHT: String = "\(urlCurrentFolder.path)/data-cht.json"
private let urlJSONBPMFReverseLookup: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup.json"
private let urlJSONBPMFReverseLookupCNS1: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS1.json"
private let urlJSONBPMFReverseLookupCNS2: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS2.json"
private let urlJSONBPMFReverseLookupCNS3: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS3.json"
private let urlJSONBPMFReverseLookupCNS4: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS4.json"
private let urlJSONBPMFReverseLookupCNS5: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS5.json"
private let urlJSONBPMFReverseLookupCNS6: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS6.json"
private var isReverseLookupDictionaryProcessed: Bool = false
private let urlSQLite: String = "\(urlCurrentFolder.path)/Build/Release/vChewingFactoryDatabase.sqlite"
private var mapReverseLookupForCheck: [String: [String]] = [:]
private var exceptedChars: Set<String> = .init()
private var ptrSQL: OpaquePointer?
var rangeMapJSONCHS: [String: [String]] = [:]
var rangeMapJSONCHT: [String: [String]] = [:]
var rangeMapSymbols: [String: [String]] = [:]
var rangeMapZhuyinwen: [String: [String]] = [:]
var rangeMapCNS: [String: [String]] = [:]
var rangeMapReverseLookup: [String: [String]] = [:]
/// Also use mapReverseLookupForCheck.
// MARK: -
func prepareDatabase() -> Bool {
let sqlMakeTableMACV = """
DROP TABLE IF EXISTS DATA_REV;
DROP TABLE IF EXISTS DATA_MAIN;
CREATE TABLE IF NOT EXISTS DATA_MAIN (
theKey TEXT NOT NULL,
theDataCHS TEXT,
theDataCHT TEXT,
theDataCNS TEXT,
theDataMISC TEXT,
theDataSYMB TEXT,
theDataCHEW TEXT,
PRIMARY KEY (theKey)
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS DATA_REV (
theChar TEXT NOT NULL,
theReadings TEXT NOT NULL,
PRIMARY KEY (theChar)
) WITHOUT ROWID;
"""
guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
guard "begin;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
return true
}
@discardableResult func writeMainMapToSQL(_ theMap: [String: [String]], column columnName: String) -> Bool {
for (encryptedKey, arrValues) in theMap {
// SQL 西 ASCII 退''
let safeKey = encryptedKey.replacingOccurrences(of: "'", with: "''")
let valueText = arrValues.joined(separator: "\t").replacingOccurrences(of: "'", with: "''")
let sqlStmt = "INSERT INTO DATA_MAIN (theKey, \(columnName)) VALUES ('\(safeKey)', '\(valueText)') ON CONFLICT(theKey) DO UPDATE SET \(columnName)='\(valueText)';"
guard sqlStmt.runAsSQLExec(dbPointer: &ptrSQL) else {
print("Failed: " + sqlStmt)
return false
}
}
return true
}
@discardableResult func writeRevLookupMapToSQL(_ theMap: [String: [String]]) -> Bool {
for (encryptedKey, arrValues) in theMap {
// SQL 西 ASCII 退''
let safeKey = encryptedKey.replacingOccurrences(of: "'", with: "''")
let valueText = arrValues.joined(separator: "\t").replacingOccurrences(of: "'", with: "''")
let sqlStmt = "INSERT INTO DATA_REV (theChar, theReadings) VALUES ('\(safeKey)', '\(valueText)') ON CONFLICT(theChar) DO UPDATE SET theReadings='\(valueText)';"
guard sqlStmt.runAsSQLExec(dbPointer: &ptrSQL) else {
print("Failed: " + sqlStmt)
return false
}
}
return true
}
// MARK: - Dump SQLite3 Memory Database to File.
@discardableResult func dumpSQLDB() -> Bool {
var ptrSQLTarget: OpaquePointer?
defer { sqlite3_close_v2(ptrSQLTarget) }
guard sqlite3_open(urlSQLite, &ptrSQLTarget) == SQLITE_OK else { return false }
let ptrBackupObj = sqlite3_backup_init(ptrSQLTarget, "main", ptrSQL, "main")
if ptrBackupObj != nil {
sqlite3_backup_step(ptrBackupObj, -1)
sqlite3_backup_finish(ptrBackupObj)
}
return sqlite3_errcode(ptrSQLTarget) == SQLITE_OK
}
// MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
@ -319,8 +413,10 @@ func rawDictForKanjis(isCHS: Bool) -> [Unigram] {
if !isReverseLookupDictionaryProcessed {
do {
isReverseLookupDictionaryProcessed = true
try JSONSerialization.data(withJSONObject: mapReverseLookupJSON, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookup))
if compileJSON {
try JSONSerialization.data(withJSONObject: mapReverseLookupJSON, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookup))
}
mapReverseLookupForCheck = mapReverseLookupUnencrypted
} catch {
NSLog(" - Core Reverse Lookup Data Generation Failed.")
@ -459,10 +555,8 @@ func fileOutput(isCHS: Bool) {
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
var strPunctuation = ""
var rangeMapJSON: [String: [String]] = [:]
let pathOutput = urlCurrentFolder.appendingPathComponent(
isCHS ? urlOutputCHS : urlOutputCHT)
let jsonURL = urlCurrentFolder.appendingPathComponent(
isCHS ? urlJSONCHS : urlJSONCHT)
let pathOutput = URL(fileURLWithPath: isCHS ? urlOutputCHS : urlOutputCHT)
let jsonURL = URL(fileURLWithPath: isCHS ? urlJSONCHS : urlJSONCHT)
var strPrintLine = ""
//
do {
@ -532,11 +626,18 @@ func fileOutput(isCHS: Bool) {
NSLog(" - \(i18n): 要寫入檔案的 txt 內容編譯完畢。")
do {
try strPrintLine.write(to: pathOutput, atomically: true, encoding: .utf8)
try JSONSerialization.data(withJSONObject: rangeMapJSON, options: .sortedKeys).write(to: jsonURL)
if compileJSON {
try JSONSerialization.data(withJSONObject: rangeMapJSON, options: .sortedKeys).write(to: jsonURL)
}
if isCHS {
rangeMapJSONCHS = rangeMapJSON
} else {
rangeMapJSONCHT = rangeMapJSON
}
} catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
}
NSLog(" - \(i18n): 寫入完成。")
NSLog(" - \(i18n): JSON & TXT 寫入完成。")
if !arrFoundedDuplications.isEmpty {
NSLog(" - \(i18n): 尋得下述重複項目,請務必手動排查:")
print("-------------------")
@ -576,7 +677,9 @@ func commonFileOutput() {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
mapSymbols[cnvPhonabetToASCII(theKey), default: []].append(theValue)
let encryptedKey = cnvPhonabetToASCII(theKey)
mapSymbols[encryptedKey, default: []].append(theValue)
rangeMapSymbols[encryptedKey, default: []].append(theValue)
}
}
}
@ -587,7 +690,9 @@ func commonFileOutput() {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
mapZhuyinwen[cnvPhonabetToASCII(theKey), default: []].append(theValue)
let encryptedKey = cnvPhonabetToASCII(theKey)
mapZhuyinwen[encryptedKey, default: []].append(theValue)
rangeMapZhuyinwen[encryptedKey, default: []].append(theValue)
}
}
}
@ -598,30 +703,33 @@ func commonFileOutput() {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
mapCNS[cnvPhonabetToASCII(theKey), default: []].append(theValue)
let encryptedKey = cnvPhonabetToASCII(theKey)
mapCNS[encryptedKey, default: []].append(theValue)
rangeMapCNS[encryptedKey, default: []].append(theValue)
json: if !theKey.contains("_"), !theKey.contains("-") {
rangeMapReverseLookup[theValue, default: []].append(encryptedKey)
if mapReverseLookupCNS1.keys.count <= 16500 {
mapReverseLookupCNS1[theValue, default: []].append(cnvPhonabetToASCII(theKey))
mapReverseLookupCNS1[theValue, default: []].append(encryptedKey)
break json
}
if mapReverseLookupCNS2.keys.count <= 16500 {
mapReverseLookupCNS2[theValue, default: []].append(cnvPhonabetToASCII(theKey))
mapReverseLookupCNS2[theValue, default: []].append(encryptedKey)
break json
}
if mapReverseLookupCNS3.keys.count <= 16500 {
mapReverseLookupCNS3[theValue, default: []].append(cnvPhonabetToASCII(theKey))
mapReverseLookupCNS3[theValue, default: []].append(encryptedKey)
break json
}
if mapReverseLookupCNS4.keys.count <= 16500 {
mapReverseLookupCNS4[theValue, default: []].append(cnvPhonabetToASCII(theKey))
mapReverseLookupCNS4[theValue, default: []].append(encryptedKey)
break json
}
if mapReverseLookupCNS5.keys.count <= 16500 {
mapReverseLookupCNS5[theValue, default: []].append(cnvPhonabetToASCII(theKey))
mapReverseLookupCNS5[theValue, default: []].append(encryptedKey)
break json
}
if mapReverseLookupCNS6.keys.count <= 16500 {
mapReverseLookupCNS6[theValue, default: []].append(cnvPhonabetToASCII(theKey))
mapReverseLookupCNS6[theValue, default: []].append(encryptedKey)
break json
}
}
@ -630,62 +738,32 @@ func commonFileOutput() {
}
NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。")
do {
try JSONSerialization.data(withJSONObject: mapSymbols, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONSymbols))
try JSONSerialization.data(withJSONObject: mapZhuyinwen, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONZhuyinwen))
try JSONSerialization.data(withJSONObject: mapCNS, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONCNS))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS1, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS1))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS2, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS2))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS3, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS3))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS4, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS4))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS5, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS5))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS6, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS6))
if compileJSON {
try JSONSerialization.data(withJSONObject: mapSymbols, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONSymbols))
try JSONSerialization.data(withJSONObject: mapZhuyinwen, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONZhuyinwen))
try JSONSerialization.data(withJSONObject: mapCNS, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONCNS))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS1, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS1))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS2, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS2))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS3, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS3))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS4, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS4))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS5, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS5))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS6, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS6))
}
} catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
}
NSLog(" - \(i18n): 寫入完成。")
}
// MARK: -
func main() {
let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
group.leave()
}
//
_ = group.wait(timeout: .distantFuture)
group.notify(queue: DispatchQueue.main) {
NSLog("// 全部辭典檔案建置完畢。")
}
}
main()
// MARK: -
func healthCheck(_ data: [Unigram]) -> String {
@ -979,3 +1057,107 @@ func healthCheck(_ data: [Unigram]) -> String {
result += "\n"
return result
}
// MARK: - Flags
struct TaskFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let common = TaskFlags(rawValue: 1 << 0)
public static let chs = TaskFlags(rawValue: 1 << 1)
public static let cht = TaskFlags(rawValue: 1 << 2)
}
// MARK: -
var compileJSON = false
var compileSQLite = true
func main() {
let arguments = CommandLine.arguments.compactMap { $0.lowercased() }
let jsonConditionMet = arguments.contains(where: { $0 == "--json" || $0 == "json" })
if jsonConditionMet {
NSLog("// 接下來準備建置 JSON 格式的原廠辭典,同時生成用來偵錯的 TXT 副產物。")
compileJSON = true
compileSQLite = false
} else {
NSLog("// 接下來準備建置 SQLite 格式的原廠辭典,同時生成用來偵錯的 TXT 副產物。")
compileJSON = false
compileSQLite = true
}
let prepared = prepareDatabase()
if compileSQLite, !prepared {
NSLog("// SQLite 資料庫初期化失敗。")
exit(-1)
}
var taskFlags: TaskFlags = [.common, .chs, .cht] {
didSet {
guard taskFlags.isEmpty else { return }
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
if !dumpSQLDB() {
NSLog("// SQLite 辭典傾印失敗。")
} else {
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
sqlite3_close_v2(ptrSQL)
}
}
}
let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
taskFlags.remove(.common)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
taskFlags.remove(.cht)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
taskFlags.remove(.chs)
group.leave()
}
//
group.wait()
}
main()

View File

@ -1,199 +0,0 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import IMKUtils
import InputMethodKit
import SwiftExtension
public let kTargetBin = "vChewing"
public let kTargetBinPhraseEditor = "vChewingPhraseEditor"
public let kTargetType = "app"
public let kTargetBundle = "vChewing.app"
public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
public let realHomeDir = URL(
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
)
public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
.appendingPathComponent(kTargetBin)
public let kDestinationPartial = urlDestinationPartial.path
public let kTargetPartialPath = urlTargetPartial.path
public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
public let kTranslocationRemovalTickInterval: TimeInterval = 0.5
public let kTranslocationRemovalDeadline: TimeInterval = 60.0
@NSApplicationMain
@objc(AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet var installButton: NSButton!
@IBOutlet var cancelButton: NSButton!
@IBOutlet var progressSheet: NSWindow!
@IBOutlet var progressIndicator: NSProgressIndicator!
@IBOutlet var appVersionLabel: NSTextField!
@IBOutlet var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
var installingVersion = ""
var translocationRemovalStartTime: Date?
var currentVersionNumber: Int = 0
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) }
}
func runAlertPanel(title: String, message: String, buttonTitle: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
func applicationDidFinishLaunching(_: Notification) {
guard
let window = window,
let cell = installButton.cell as? NSButtonCell,
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String,
let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String,
let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String
else {
NSSound.beep()
NSLog("The vChewing App Installer failed its initial guard-let process on appDidFinishLaunching().")
return
}
self.installingVersion = installingVersion
cancelButton.nextKeyView = installButton
installButton.nextKeyView = cancelButton
window.defaultButtonCell = cell
appCopyrightLabel.stringValue = copyrightLabel
appEULAContent.string = eulaContent + "\n" + eulaContentUpstream
appVersionLabel.stringValue = "\(versionString) Build \(installingVersion)"
window.title = "\(window.title) (v\(versionString), Build \(installingVersion))"
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
window.titlebarAppearsTransparent = true
if FileManager.default.fileExists(atPath: kTargetPartialPath) {
let currentBundle = Bundle(path: kTargetPartialPath)
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0
if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{
// Upgrading confirmed.
installButton.title = NSLocalizedString("Upgrade", comment: "")
}
}
window.center()
window.orderFront(self)
NSApp.popup()
}
@IBAction func agreeAndInstallAction(_: AnyObject) {
cancelButton.isEnabled = false
installButton.isEnabled = false
removeThenInstallInputMethod()
}
@objc func timerTick(_ timer: Timer) {
guard let window = window else { return }
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date())
if elapsed >= kTranslocationRemovalDeadline {
timer.invalidate()
window.endSheet(progressSheet, returnCode: .cancel)
} else if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0
timer.invalidate()
window.endSheet(progressSheet, returnCode: .continue)
}
}
func endAppWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
NSApp.terminate(self)
}
}
@IBAction func cancelAction(_: AnyObject) {
NSApp.terminate(self)
}
func windowWillClose(_: Notification) {
NSApp.terminate(self)
}
func shell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
} else {
task.launchPath = "/bin/zsh"
}
task.standardInput = nil
if #available(macOS 10.13, *) {
try task.run()
} else {
task.launch()
}
var output = ""
do {
let data = try pipe.fileHandleForReading.readToEnd()
if let data = data, let str = String(data: data, encoding: .utf8) {
output.append(str)
}
} catch {
return ""
}
return output
}
}
// MARK: - NSApp Activation Helper
// This is to deal with changes brought by macOS 14.
private extension NSApplication {
func popup() {
#if compiler(>=5.9) && canImport(AppKit, _version: "14.0")
if #available(macOS 14.0, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
}
}

View File

@ -0,0 +1,171 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import InputMethodKit
import SwiftUI
public let kTargetBin = "vChewing"
public let kTargetBinPhraseEditor = "vChewingPhraseEditor"
public let kTargetType = "app"
public let kTargetBundle = "vChewing.app"
public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
public let kTISInputSourceID = "org.atelierInmu.inputmethod.vChewing"
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
public let realHomeDir = URL(
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
)
public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
.appendingPathComponent(kTargetBin)
public let kDestinationPartial = urlDestinationPartial.path
public let kTargetPartialPath = urlTargetPartial.path
public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
public let kTranslocationRemovalTickInterval: TimeInterval = 0.5
public let kTranslocationRemovalDeadline: TimeInterval = 60.0
public let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? "BAD_INSTALLING_VER"
public let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "BAD_VER_STR"
public let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
public let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String ?? "BAD_EULA_CONTENT"
public let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String ?? "BAD_EULA_UPSTREAM"
public var mainWindowTitle: String {
"i18n:installer.INSTALLER_APP_TITLE_FULL".i18n + " (v\(versionString), Build \(installingVersion))"
}
var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description))
}
// MARK: - NSApp Activation Helper
// This is to deal with changes brought by macOS 14.
public extension NSApplication {
func popup() {
#if compiler(>=5.9) && canImport(AppKit, _version: "14.0")
if #available(macOS 14.0, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
}
}
// MARK: - KeyWindow Finder
public extension NSApplication {
var keyWindows: [NSWindow] {
NSApp.windows.filter(\.isKeyWindow)
}
}
// MARK: - NSApp End With Delay
public extension NSApplication {
func terminateWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { [weak self] in
if let this = self {
this.terminate(this)
}
}
}
}
// MARK: - Alert Message & Title Structure
public struct AlertIntel {}
public enum AlertType: String, Identifiable {
public var id: String { rawValue }
case nothing, installationFailed, missingAfterRegistration, postInstallAttention, postInstallWarning, postInstallOK
var title: LocalizedStringKey {
switch self {
case .nothing: return ""
case .installationFailed: return "Install Failed"
case .missingAfterRegistration: return "Fatal Error"
case .postInstallAttention: return "Attention"
case .postInstallWarning: return "Warning"
case .postInstallOK: return "Installation Successful"
}
}
var message: String {
switch self {
case .nothing: return ""
case .installationFailed:
return "Cannot copy the file to the destination.".i18n
case .missingAfterRegistration:
return String(
format: "Cannot find input source %@ after registration.".i18n,
kTISInputSourceID
)
case .postInstallAttention:
return "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.".i18n
case .postInstallWarning:
return "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.".i18n
case .postInstallOK:
return "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.".i18n
}
}
}
private extension StringLiteralType {
var i18n: String { NSLocalizedString(description, comment: "") }
}
// MARK: - Shell
public extension NSApplication {
func shell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
} else {
task.launchPath = "/bin/zsh"
}
task.standardInput = nil
if #available(macOS 10.13, *) {
try task.run()
} else {
task.launch()
}
var output = ""
do {
let data = try pipe.fileHandleForReading.readToEnd()
if let data = data, let str = String(data: data, encoding: .utf8) {
output.append(str)
}
} catch {
return ""
}
return output
}
}

161
Installer/MainView.swift Normal file
View File

@ -0,0 +1,161 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import SwiftUI
public struct MainView: View {
static let strCopyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
@State var pendingSheetPresenting = false
@State var isShowingAlertForFailedInstallation = false
@State var isShowingAlertForMissingPostInstall = false
@State var isShowingPostInstallNotification = false
@State var currentAlertContent: AlertType = .nothing
@State var isCancelButtonEnabled = true
@State var isAgreeButtonEnabled = true
@State var isPreviousVersionNotFullyDeactivated = false
@State var isTranslocationFinished: Bool?
@State var isUpgrading: Bool = false
var translocationRemovalStartTime: Date?
@State var timeRemaining = 60
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
public init() {
if FileManager.default.fileExists(atPath: kTargetPartialPath) {
let currentBundle = Bundle(path: kTargetPartialPath)
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{
isUpgrading = true
}
}
}
public var body: some View {
GroupBox {
VStack(alignment: .leading, spacing: 6) {
VStack(alignment: .leading) {
HStack(alignment: .center) {
if let icon = NSImage(named: "IconSansMargin") {
Image(nsImage: icon).resizable().frame(width: 90, height: 90)
}
VStack(alignment: .leading) {
HStack {
Text("i18n:installer.APP_NAME").fontWeight(.heavy).lineLimit(1)
Text("v\(versionString) Build \(installingVersion)").lineLimit(1)
}.fixedSize()
Text("i18n:installer.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
Text(Self.strCopyrightLabel).font(.custom("Tahoma", size: 11))
Text("i18n:installer.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
}
}
GroupBox(label: Text("i18n:installer.LICENSE_TITLE")) {
ScrollView(.vertical, showsIndicators: true) {
HStack {
Text(eulaContent + "\n" + eulaContentUpstream).textSelection(.enabled)
.frame(maxWidth: 455)
.font(.custom("Tahoma", size: 11))
Spacer()
}
}.padding(4).frame(height: 128)
}
Text("i18n:installer.EULA_PROMPT_NOTICE").bold().padding(.bottom, 2)
}
Divider()
HStack(alignment: .top) {
Text("i18n:installer.DISCLAIMER_TEXT")
.font(.custom("Tahoma", size: 11))
.opacity(0.5)
.frame(maxWidth: .infinity)
VStack(spacing: 4) {
Button { installationButtonClicked() } label: {
Text(isUpgrading ? "i18n:installer.DO_APP_UPGRADE" : "i18n:installer.ACCEPT_INSTALLATION")
.bold().frame(width: 114)
}
.keyboardShortcut(.defaultAction)
.disabled(!isCancelButtonEnabled)
Button(role: .cancel) { NSApp.terminateWithDelay() } label: {
Text("i18n:installer.CANCEL_INSTALLATION").frame(width: 114)
}
.keyboardShortcut(.cancelAction)
.disabled(!isAgreeButtonEnabled)
}.fixedSize(horizontal: true, vertical: true)
}
Spacer()
}
.font(.custom("Tahoma", size: 12))
.padding(4)
}
// ALERTS
.alert(AlertType.installationFailed.title, isPresented: $isShowingAlertForFailedInstallation) {
Button(role: .cancel) { NSApp.terminateWithDelay() } label: { Text("Cancel") }
} message: {
Text(AlertType.installationFailed.message)
}
.alert(AlertType.missingAfterRegistration.title, isPresented: $isShowingAlertForMissingPostInstall) {
Button(role: .cancel) { NSApp.terminateWithDelay() } label: { Text("Abort") }
} message: {
Text(AlertType.missingAfterRegistration.message)
}
.alert(currentAlertContent.title, isPresented: $isShowingPostInstallNotification) {
Button(role: .cancel) { NSApp.terminateWithDelay() } label: {
Text(currentAlertContent == .postInstallWarning ? "Continue" : "OK")
}
} message: {
Text(currentAlertContent.message)
}
// SHEET FOR STOPPING THE OLD VERSION
.sheet(isPresented: $pendingSheetPresenting) {
// TODO: Tasks after sheet gets closed by `dismiss()`.
} content: {
Text("i18n:installer.STOPPING_THE_OLD_VERSION").frame(width: 407, height: 144)
.onReceive(timer) { _ in
if timeRemaining > 0 {
if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
pendingSheetPresenting = false
isTranslocationFinished = true
installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: false
)
}
timeRemaining -= 1
} else {
pendingSheetPresenting = false
isTranslocationFinished = false
installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: true
)
}
}
}
// OTHER
.padding(12)
.frame(width: 533, alignment: .topLeading)
.navigationTitle(mainWindowTitle)
.fixedSize()
.foregroundStyle(Color(nsColor: NSColor.textColor))
.background(Color(nsColor: NSColor.windowBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 16))
.frame(minWidth: 533, idealWidth: 533, maxWidth: 533,
minHeight: 386, idealHeight: 386, maxHeight: 386,
alignment: .top)
}
func installationButtonClicked() {
isCancelButtonEnabled = false
isAgreeButtonEnabled = false
removeThenInstallInputMethod()
}
}

View File

@ -1,5 +1,3 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
@ -9,22 +7,12 @@
// requirements defined in MIT License.
import AppKit
import IMKUtils
import InputMethodKit
extension AppDelegate {
public extension MainView {
func removeThenInstallInputMethod() {
// if !FileManager.default.fileExists(atPath: kTargetPartialPath) {
// installInputMethod(
// previousExists: false, previousVersionNotFullyDeactivatedWarning: false
// )
// return
// }
guard let window = window else { return }
let shouldWaitForTranslocationRemoval =
Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath)
&& window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:)))
let shouldWaitForTranslocationRemoval = Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath)
//
do {
@ -58,28 +46,7 @@ extension AppDelegate {
killTask2.waitUntilExit()
if shouldWaitForTranslocationRemoval {
progressIndicator.startAnimation(self)
window.beginSheet(progressSheet) { returnCode in
DispatchQueue.main.async {
if returnCode == .continue {
self.installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: false
)
} else {
self.installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: true
)
}
}
}
translocationRemovalStartTime = Date()
Timer.scheduledTimer(
timeInterval: kTranslocationRemovalTickInterval, target: self,
selector: #selector(timerTick(_:)), userInfo: nil, repeats: true
)
pendingSheetPresenting = true
} else {
installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false
@ -105,20 +72,16 @@ extension AppDelegate {
cpTask.waitUntilExit()
if cpTask.terminationStatus != 0 {
runAlertPanel(
title: NSLocalizedString("Install Failed", comment: ""),
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
buttonTitle: NSLocalizedString("Cancel", comment: "")
)
endAppWithDelay()
isShowingAlertForFailedInstallation = true
NSApp.terminateWithDelay()
}
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
_ = try? NSApp.shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
guard let theBundle = Bundle(url: imeURLInstalled),
let imeIdentifier = theBundle.bundleIdentifier
else {
endAppWithDelay()
NSApp.terminateWithDelay()
return
}
@ -128,18 +91,8 @@ extension AppDelegate {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
if !status {
let message = String(
format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: ""
),
imeIdentifier
)
runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "")
)
endAppWithDelay()
return
isShowingAlertForMissingPostInstall = true
NSApp.terminateWithDelay()
}
if allRegisteredInstancesOfThisInputMethod.isEmpty {
@ -172,35 +125,14 @@ extension AppDelegate {
}
// Alert Panel
let ntfPostInstall = NSAlert()
if warning {
ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "")
ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
currentAlertContent = .postInstallAttention
} else if !mainInputSourceEnabled {
currentAlertContent = .postInstallWarning
} else {
if !mainInputSourceEnabled {
ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "")
ntfPostInstall.informativeText = NSLocalizedString(
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
} else {
ntfPostInstall.messageText = NSLocalizedString(
"Installation Successful", comment: ""
)
ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
}
}
ntfPostInstall.beginSheetModal(for: window!) { _ in
self.endAppWithDelay()
currentAlertContent = .postInstallOK
}
isShowingPostInstallNotification = true
NSApp.terminateWithDelay()
}
}

View File

@ -1,366 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21225" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21225"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="494" id="495"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu title="AMainMenu" systemMenu="main" id="29">
<items>
<menuItem title="vChewing Installer" id="56">
<menu key="submenu" title="vChewing Installer" systemMenu="apple" id="57">
<items>
<menuItem title="About vChewing Installer" id="58">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="236">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Services" id="131">
<menu key="submenu" title="Services" systemMenu="services" id="130"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="144">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Hide vChewing Installer" keyEquivalent="h" id="134">
<connections>
<action selector="hide:" target="-1" id="367"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="145">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="368"/>
</connections>
</menuItem>
<menuItem title="Show All" id="150">
<connections>
<action selector="unhideAllApplications:" target="-1" id="370"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="149">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Quit vChewing Installer" keyEquivalent="q" id="136">
<connections>
<action selector="terminate:" target="-3" id="449"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="83"/>
<menuItem title="Edit" id="LJX-Bb-mhU">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="6QY-kP-PQQ">
<items>
<menuItem title="Undo" keyEquivalent="z" id="5tq-5G-Yoy">
<connections>
<action selector="undo:" target="-1" id="P9n-jj-WpM"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="GRe-Pk-1EX">
<connections>
<action selector="redo:" target="-1" id="cbT-AB-slM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="cYt-uT-CAh"/>
<menuItem title="Cut" keyEquivalent="x" id="zAh-7y-AvL">
<connections>
<action selector="cut:" target="-1" id="arZ-EA-CgM"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="WoU-zb-uKy">
<connections>
<action selector="copy:" target="-1" id="0JC-Jc-0Xl"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="Fid-E7-Ykc">
<connections>
<action selector="paste:" target="-1" id="fVk-V0-Sbq"/>
</connections>
</menuItem>
<menuItem title="Delete" id="Ier-IT-JZa">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="X7x-wD-fWC"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="ZsT-7a-SE6">
<connections>
<action selector="selectAll:" target="-1" id="iwd-aI-lml"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="139" y="154"/>
</menu>
<window title="vChewing Installer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="371">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="335" y="390" width="533" height="457"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<value key="minSize" type="size" width="533" height="457"/>
<value key="maxSize" type="size" width="533" height="457"/>
<view key="contentView" id="372">
<rect key="frame" x="0.0" y="0.0" width="533" height="457"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="575">
<rect key="frame" x="378" y="101" width="147" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="133" id="S4F-Qe-xuk"/>
</constraints>
<buttonCell key="cell" type="push" title="I Accept" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="576">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" size="13" name="Tahoma-Bold"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="agreeAndInstallAction:" target="494" id="708"/>
</connections>
</button>
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" findBarPosition="belowContent" translatesAutoresizingMaskIntoConstraints="NO" id="YCR-wo-M5a">
<rect key="frame" x="91" y="165" width="427" height="196"/>
<clipView key="contentView" drawsBackground="NO" id="NrY-FL-PVu" userLabel="appEULAContentClip">
<rect key="frame" x="1" y="1" width="425" height="194"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" findStyle="bar" smartInsertDelete="YES" id="47J-tO-8TZ" userLabel="appEULAContent">
<rect key="frame" x="0.0" y="-2" width="425" height="194"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="425" height="194"/>
<size key="maxSize" width="427" height="10000000"/>
<attributedString key="textStorage">
<fragment content="Placeholder for EULA Texts.">
<attributes>
<color key="NSColor" name="textColor" catalog="System" colorSpace="catalog"/>
<font key="NSFont" metaFont="systemLight" size="11"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="YOZ-MC-EF2">
<rect key="frame" x="-100" y="-100" width="240" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" controlSize="mini" horizontal="NO" id="E5B-3B-faV">
<rect key="frame" x="412" y="1" width="14" height="194"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ir5-sQ-sJc">
<rect key="frame" x="89" y="443" width="130" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="vChewing for macOS" id="GNc-8S-1VG" userLabel="appNameLabel">
<font key="font" size="12" name="Tahoma-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bzR-Oa-BZa">
<rect key="frame" x="89" y="428" width="362" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Was derived from OpenVanilla McBopopmofo Project (MIT-License)." id="QYf-Nf-hoi">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="03l-rN-zf9">
<rect key="frame" x="89" y="413" width="297" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="293" id="v2b-OK-WGD"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="Placeholder for showing copyright information." id="eo3-TK-0rB" userLabel="appCopyrightLabel">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XLb-mv-73s">
<rect key="frame" x="89" y="391" width="431" height="14"/>
<textFieldCell key="cell" title="Placeholder for detailed credits." id="VW8-s5-Wpn">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Yyh-Nw-Sba">
<rect key="frame" x="15" y="137" width="503" height="5"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="k5O-zZ-gQY">
<rect key="frame" x="89" y="369" width="431" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MIT-NTL License:" id="AVS-ih-FXM">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="miu-08-dZk">
<rect key="frame" x="13" y="47" width="360" height="84"/>
<constraints>
<constraint firstAttribute="width" constant="356" id="pu3-zr-hJy"/>
</constraints>
<textFieldCell key="cell" id="Q9M-ni-kUM">
<font key="font" size="12" name="Tahoma"/>
<string key="title">DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="Ked-gt-bjE">
<rect key="frame" x="15" y="147" width="63" height="310"/>
<constraints>
<constraint firstAttribute="width" constant="63" id="fgC-vo-Ho8"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="AboutBanner" id="akk-zO-Abm"/>
</imageView>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="z1m-8k-Z63">
<rect key="frame" x="218" y="443" width="126" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="122" id="yKq-Fv-W1J"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="version_placeholder" id="JRP-At-H9q" userLabel="appVersionLabel">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nul-TQ-gOI">
<rect key="frame" x="89" y="148" width="431" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="By installing the software, you must accept the terms above." id="mf8-6e-z7X">
<font key="font" size="12" name="Tahoma-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="592">
<rect key="frame" x="378" y="74" width="147" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="593">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" size="13" name="Tahoma"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancelAction:" target="494" id="707"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="575" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="miu-08-dZk" secondAttribute="trailing" constant="8" symbolic="YES" id="18X-Qf-xUC"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="top" secondItem="Ked-gt-bjE" secondAttribute="bottom" constant="7" id="1Hx-8o-xpF"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="top" secondItem="nul-TQ-gOI" secondAttribute="bottom" constant="8" symbolic="YES" id="1Mz-Yp-lqA"/>
<constraint firstItem="575" firstAttribute="leading" secondItem="592" secondAttribute="leading" id="2Kf-DA-DXH"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="leading" secondItem="372" secondAttribute="leading" constant="15" id="2ne-pt-ddK"/>
<constraint firstItem="03l-rN-zf9" firstAttribute="leading" secondItem="XLb-mv-73s" secondAttribute="leading" id="6Mv-X8-W55"/>
<constraint firstItem="nul-TQ-gOI" firstAttribute="trailing" secondItem="Yyh-Nw-Sba" secondAttribute="trailing" id="6yu-Wm-g26"/>
<constraint firstItem="592" firstAttribute="top" secondItem="575" secondAttribute="bottom" constant="7" id="7Q7-30-gTO"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bzR-Oa-BZa" secondAttribute="trailing" constant="20" symbolic="YES" id="8td-FZ-tnM"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="baseline" secondItem="z1m-8k-Z63" secondAttribute="baseline" id="9AX-QJ-G9U"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="leading" secondItem="Ked-gt-bjE" secondAttribute="trailing" constant="13" id="Brw-UI-0WK"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="trailing" secondItem="YCR-wo-M5a" secondAttribute="trailing" id="DfC-Ke-tb5"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="leading" secondItem="YCR-wo-M5a" secondAttribute="leading" id="FIo-Op-SV8"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="leading" secondItem="miu-08-dZk" secondAttribute="leading" id="H4v-4O-xZY"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="trailing" secondItem="nul-TQ-gOI" secondAttribute="trailing" id="HX6-hi-PJs"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="z1m-8k-Z63" secondAttribute="trailing" constant="20" symbolic="YES" id="Hg9-8d-O7s"/>
<constraint firstItem="z1m-8k-Z63" firstAttribute="leading" secondItem="ir5-sQ-sJc" secondAttribute="trailing" constant="3" id="LSc-gD-CbY"/>
<constraint firstItem="575" firstAttribute="top" secondItem="Yyh-Nw-Sba" secondAttribute="bottom" constant="11" id="Nw2-bH-vTF"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="03l-rN-zf9" secondAttribute="trailing" constant="20" symbolic="YES" id="PRC-Y1-rIz"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="leading" secondItem="Yyh-Nw-Sba" secondAttribute="leading" id="SKi-gn-JeS"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="trailing" secondItem="k5O-zZ-gQY" secondAttribute="trailing" id="VOo-Q9-rki"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="top" secondItem="372" secondAttribute="top" id="WPX-gk-uqh"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="top" secondItem="03l-rN-zf9" secondAttribute="bottom" constant="8" symbolic="YES" id="bJX-f1-1PU"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="top" secondItem="ir5-sQ-sJc" secondAttribute="top" id="caT-rm-xEa"/>
<constraint firstItem="bzR-Oa-BZa" firstAttribute="top" secondItem="ir5-sQ-sJc" secondAttribute="bottom" constant="1" id="dyJ-9C-f56"/>
<constraint firstItem="bzR-Oa-BZa" firstAttribute="leading" secondItem="03l-rN-zf9" secondAttribute="leading" id="etY-2E-2Sa"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="leading" secondItem="nul-TQ-gOI" secondAttribute="leading" id="fl0-wm-8Pa"/>
<constraint firstItem="miu-08-dZk" firstAttribute="top" secondItem="Yyh-Nw-Sba" secondAttribute="bottom" constant="8" symbolic="YES" id="lY7-Se-lpo"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="leading" secondItem="k5O-zZ-gQY" secondAttribute="leading" id="qzW-qc-9yQ"/>
<constraint firstItem="575" firstAttribute="trailing" secondItem="592" secondAttribute="trailing" id="sIp-L2-QLj"/>
<constraint firstItem="575" firstAttribute="trailing" secondItem="Yyh-Nw-Sba" secondAttribute="trailing" id="tap-mY-bvB"/>
<constraint firstItem="nul-TQ-gOI" firstAttribute="top" secondItem="YCR-wo-M5a" secondAttribute="bottom" constant="3" id="tqc-zq-Egb"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="top" secondItem="k5O-zZ-gQY" secondAttribute="bottom" constant="8" symbolic="YES" id="u3L-Nh-ELP"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="top" secondItem="XLb-mv-73s" secondAttribute="bottom" constant="8" symbolic="YES" id="uxg-3X-XtF"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="leading" secondItem="bzR-Oa-BZa" secondAttribute="leading" id="vkK-Vc-WOf"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="centerX" secondItem="372" secondAttribute="centerX" id="xqG-pX-j0d"/>
<constraint firstItem="03l-rN-zf9" firstAttribute="top" secondItem="bzR-Oa-BZa" secondAttribute="bottom" constant="1" id="yKI-MD-nsC"/>
</constraints>
</view>
<connections>
<outlet property="delegate" destination="494" id="706"/>
</connections>
<point key="canvasLocation" x="-306.5" y="-230"/>
</window>
<customObject id="494" customClass="AppDelegate">
<connections>
<outlet property="appCopyrightLabel" destination="03l-rN-zf9" id="XS5-cZ-k9H"/>
<outlet property="appEULAContent" destination="47J-tO-8TZ" id="kRU-X2-8kX"/>
<outlet property="appVersionLabel" destination="z1m-8k-Z63" id="75X-uy-0Iz"/>
<outlet property="cancelButton" destination="592" id="710"/>
<outlet property="installButton" destination="575" id="709"/>
<outlet property="progressIndicator" destination="deb-uT-yNv" id="Cpk-6Z-0rj"/>
<outlet property="progressSheet" destination="gHl-Hx-eQn" id="gD4-XO-YO1"/>
<outlet property="window" destination="371" id="532"/>
</connections>
</customObject>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gHl-Hx-eQn">
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="480" height="180"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<view key="contentView" id="wAe-c8-Vh9">
<rect key="frame" x="0.0" y="0.0" width="480" height="180"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="1" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="deb-uT-yNv">
<rect key="frame" x="20" y="67" width="440" height="20"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VDL-Yq-heb">
<rect key="frame" x="18" y="94" width="444" height="17"/>
<constraints>
<constraint firstAttribute="height" constant="17" id="MLj-KG-mL8"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Stopping the old version. This may take up to one minute…" id="nTo-dx-qfZ">
<font key="font" size="13" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="VDL-Yq-heb" firstAttribute="trailing" secondItem="deb-uT-yNv" secondAttribute="trailing" id="DCe-Xh-ee1"/>
<constraint firstItem="deb-uT-yNv" firstAttribute="top" secondItem="VDL-Yq-heb" secondAttribute="bottom" constant="8" symbolic="YES" id="HUE-gU-UFS"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="top" secondItem="wAe-c8-Vh9" secondAttribute="top" constant="69" id="IwI-63-e9H"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="leading" secondItem="deb-uT-yNv" secondAttribute="leading" id="UUz-sT-D9I"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="leading" secondItem="wAe-c8-Vh9" secondAttribute="leading" constant="20" symbolic="YES" id="Vgg-bw-6wt"/>
<constraint firstAttribute="trailing" secondItem="VDL-Yq-heb" secondAttribute="trailing" constant="20" symbolic="YES" id="ft0-oZ-8HD"/>
</constraints>
</view>
<point key="canvasLocation" x="529" y="-282"/>
</window>
<customObject id="420" customClass="NSFontManager"/>
</objects>
<resources>
<image name="AboutBanner" width="63" height="310"/>
</resources>
</document>

View File

@ -1,19 +1,30 @@
"vChewing Input Method" = "vChewing Input Method";
"Upgrade" = "Accept & Upgrade";
"Abort" = "Abort";
"Attention" = "Attention";
"Cancel" = "Cancel";
"Cannot activate the input method." = "Cannot activate the input method.";
"Cannot copy the file to the destination." = "Cannot copy the file to the destination.";
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
"Continue" = "Continue";
"Fatal Error" = "Fatal Error";
"i18n:installer.ACCEPT_INSTALLATION" = "I Accept";
"i18n:installer.APP_DERIVED_FROM" = "Was derived from OpenVanilla McBopopmofo Project (MIT-License).";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "Cancel";
"i18n:installer.DEV_CREW" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License).";
"i18n:installer.DISCLAIMER_TEXT" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
"i18n:installer.DO_APP_UPGRADE" = "Accept & Upgrade";
"i18n:installer.EULA_PROMPT_NOTICE" = "By installing the software, you must accept the terms above.";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "vChewing Installer";
"i18n:installer.INSTALLER_APP_TITLE" = "vChewing Installer";
"i18n:installer.LICENSE_TITLE" = "MIT-NTL License:";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "Stopping the old version. This may take up to one minute…";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
"Install Failed" = "Install Failed";
"Installation Successful" = "Installation Successful";
"OK" = "OK";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.";
"Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…";
"Attention" = "Attention";
"vChewing Input Method" = "vChewing Input Method";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.";
"Fatal Error" = "Fatal Error";
"Abort" = "Abort";
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
"Warning" = "Warning";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
"Continue" = "Continue";

View File

@ -1,72 +0,0 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "vChewing Installer";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "vChewing Installer";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "About vChewing Installer";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "File";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "Services";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "Services";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "Hide vChewing Installer";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "Quit vChewing Installer";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "Hide Others";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "Show All";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "vChewing Installer";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "I Accept";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "Cancel";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "MIT-NTL License:";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "Was derived from OpenVanilla McBopopmofo Project (MIT-License).";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License).\nApp-style installer is derived from OpenVanilla (MIT-License).";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "Window";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "By installing the software, you must accept the terms above.";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "Stopping the old version. This may take up to one minute…";

View File

@ -1,19 +1,30 @@
"vChewing Input Method" = "威注音入力アプリ";
"Upgrade" = "承認と更新";
"Abort" = "中止";
"Attention" = "ご注意";
"Cancel" = "取消";
"Cannot activate the input method." = "入力アプリ、起動失敗。";
"Cannot copy the file to the destination." = "目標へファイルのコピーできません。";
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
"Continue" = "続行";
"Fatal Error" = "致命錯乱";
"i18n:installer.ACCEPT_INSTALLATION" = "承認する";
"i18n:installer.APP_DERIVED_FROM" = "曾て OpenVanilla 小麦注音プロジェクト (MIT-License) から派生。";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "取消";
"i18n:installer.DEV_CREW" = "macOS 版威注音の開発Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。\nウォーキング算法Lukhnos Liu (Gramambular 2, MIT-License)。";
"i18n:installer.DISCLAIMER_TEXT" = "免責事項vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
"i18n:installer.DO_APP_UPGRADE" = "承認と更新";
"i18n:installer.EULA_PROMPT_NOTICE" = "このアプリを実装するために、上記の条約を承認すべきである。";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音入力 実装用アプリ";
"i18n:installer.INSTALLER_APP_TITLE" = "威注音入力 実装用アプリ";
"i18n:installer.LICENSE_TITLE" = "MIT商標不許可ライセンス (MIT-NTL License):";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
"Install Failed" = "実装失敗。";
"Installation Successful" = "実装完了";
"OK" = "うむ";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音入力、利用準備完了。\n\nこのシステムユーザーアカウントで初めて実装した場合、再ログインしてください。";
"Stopping the old version. This may take up to one minute…" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
"Attention" = "ご注意";
"vChewing Input Method" = "威注音入力アプリ";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音入力、利用準備完了。\n\nこのシステムユーザーアカウントで初めて実装した場合、再ログインしてください。";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "威注音入力の更新は実装完了しましたが、うまく作動できるために、このパソコンの再起動および再ログインが必要だと恐れ入ります。";
"Fatal Error" = "致命錯乱";
"Abort" = "中止";
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
"Warning" = "お知らせ";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
"Continue" = "続行";

View File

@ -1,72 +0,0 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音入力 実装用アプリ";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音入力 実装用アプリ";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "威注音入力 実装用アプリについて…";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "ファイル";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "サービス";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "サービス";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "全ウィンドウ隠す";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "威注音入力 実装用アプリ を終了";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "他のアプリのウィンドウを隠す";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "隠したウィンドウを全部表示する";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音入力 実装用アプリ";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "承認する";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消";
/* Class = "NSTextFieldCell"; title = "3-Clause BSD License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "MIT商標不許可ライセンス (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
"JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免責事項vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "曾て OpenVanilla 小麦注音プロジェクト (MIT-License) から派生。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "macOS 版威注音の開発Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。\nウォーキング算法Lukhnos Liu (Gramambular 2, MIT-License)。\nApp フォーマットで出来た実装アプリは OpenVanilla (MIT-License) から受け継ぎたものである。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
"eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "Window";
/* Class = "NSTextFieldCell"; title = "By installing the software, you must accept the terms above."; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "このアプリを実装するために、上記の条約を承認すべきである。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";

View File

@ -1,19 +1,30 @@
"vChewing Input Method" = "威注音输入法";
"Upgrade" = "接受并升级";
"Abort" = "放弃安装";
"Attention" = "请注意";
"Cancel" = "取消";
"Cannot activate the input method." = "无法启用输入法。";
"Cannot copy the file to the destination." = "无法将输入法拷贝至目的地。";
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 之后仍然无法找到该输入法。";
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
"Continue" = "继续";
"Fatal Error" = "安装错误";
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
"i18n:installer.APP_DERIVED_FROM" = "该专案曾由 OpenVanilla 小麦注音专案 (MIT-License) 衍生而来。";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "取消安装";
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研发Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。\n爬轨算法Lukhnos Liu (Gramambular 2, MIT-License)。";
"i18n:installer.DISCLAIMER_TEXT" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
"i18n:installer.DO_APP_UPGRADE" = "接受并升级";
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安装该软件,请接受上述条款。";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音输入法安装程式";
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安装程式";
"i18n:installer.LICENSE_TITLE" = "麻理去商标授权合约 (MIT-NTL License):";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待旧版完全停用,大约需要一分钟…";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
"Install Failed" = "安装失败";
"Installation Successful" = "安装成功";
"OK" = "确定";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音输入法安装成功。\n\n若是在當前使用者帳戶內首次安裝的話請重新登入。";
"Stopping the old version. This may take up to one minute…" = "正在试图结束正在运行的旧版输入法,大概需要一分钟…";
"Attention" = "请注意";
"vChewing Input Method" = "威注音输入法";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音输入法安装成功。\n\n若是在當前使用者帳戶內首次安裝的話請重新登入。";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安装完成,但建议您登出或重新开机,以便顺利使用新版。";
"Fatal Error" = "安装错误";
"Abort" = "放弃安装";
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 之后仍然无法找到该输入法。";
"Warning" = "安装不完整";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
"Continue" = "继续";

View File

@ -1,72 +0,0 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音安装程式";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音安装程式";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "关于威注音安装程式";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "档案";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "服务";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "服务";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "隐藏威注音安装程式";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "结束威注音安装程式";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "隐藏其他程式";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "显示所有程式";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音输入法安装程式";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "我接受";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消安装";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "麻理去商标授权合约 (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "该专案曾由 OpenVanilla 小麦注音专案 (MIT-License) 衍生而来。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "威注音 macOS 程式研发Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。\n爬轨算法Lukhnos Liu (Gramambular 2, MIT-License)。\nApp 格式的安装程式继承自 OpenVanilla (MIT-License)。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "视窗";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "若要安装该软件,请接受上述条款。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "等待旧版完全停用,大约需要一分钟…";

View File

@ -1,19 +1,30 @@
"vChewing Input Method" = "威注音輸入法";
"Upgrade" = "接受並升級";
"Abort" = "放棄安裝";
"Attention" = "請注意";
"Cancel" = "取消";
"Cannot activate the input method." = "無法啟用輸入法。";
"Cannot copy the file to the destination." = "無法將輸入法拷貝至目的地。";
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 之後仍然無法找到該輸入法。";
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
"Continue" = "繼續";
"Fatal Error" = "安裝錯誤";
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
"i18n:installer.APP_DERIVED_FROM" = "該專案曾由 OpenVanilla 小麥注音專案 (MIT-License) 衍生而來。";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "取消安裝";
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研發Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。\n爬軌算法Lukhnos Liu (Gramambular 2, MIT-License)。";
"i18n:installer.DISCLAIMER_TEXT" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
"i18n:installer.DO_APP_UPGRADE" = "接受並升級";
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安裝該軟體,請接受上述條款。";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音輸入法安裝程式";
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安裝程式";
"i18n:installer.LICENSE_TITLE" = "麻理去商標授權合約 (MIT-NTL License):";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待舊版完全停用,大約需要一分鐘…";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
"Install Failed" = "安裝失敗";
"Installation Successful" = "安裝成功";
"OK" = "確定";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音輸入法安裝成功。\n\n若是在当前使用者帐户内首次安装的话请重新登入。";
"Stopping the old version. This may take up to one minute…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…";
"Attention" = "請注意";
"vChewing Input Method" = "威注音輸入法";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音輸入法安裝成功。\n\n若是在當前使用者帳戶內首次安裝的話請重新登入。";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安裝完成,但建議您登出或重新開機,以便順利使用新版。";
"Fatal Error" = "安裝錯誤";
"Abort" = "放棄安裝";
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 之後仍然無法找到該輸入法。";
"Warning" = "安裝不完整";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
"Continue" = "繼續";

View File

@ -1,72 +0,0 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音安裝程式";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音安裝程式";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "關於威注音安裝程式";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "檔案";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "服務";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "服務";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "隱藏威注音安裝程式";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "結束威注音安裝程式";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "隱藏其他程式";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "顯示所有程式";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音輸入法安裝程式";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "我接受";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消安裝";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "麻理去商標授權合約 (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "該專案曾由 OpenVanilla 小麥注音專案 (MIT-License) 衍生而來。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "威注音 macOS 程式研發Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。\n爬軌算法Lukhnos Liu (Gramambular 2, MIT-License)。\nApp 格式的安裝程式繼承自 OpenVanilla (MIT-License)。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "視窗";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "若要安裝該軟體,請接受上述條款。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "等待舊版完全停用,大約需要一分鐘…";

View File

@ -19,4 +19,4 @@ OS_Version=$(sw_vers -productVersion)
##### fi
# Finally, register the input method:
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install --all || true
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install || true

View File

@ -13,10 +13,10 @@ if [ "${login_user}" = root ]; then
rm -rf /Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout || true
fi
rm -rf ~/Library/Input\ Methods/vChewing.app
rm -rf ~/Library/Keyboard\ Layouts/vChewingKeyLayout.bundle
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ Dachen.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ ETen.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ FakeSeigyou.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ IBM.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout
rm -rf ~/Library/Input\ Methods/vChewing.app || true
rm -rf ~/Library/Keyboard\ Layouts/vChewingKeyLayout.bundle || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ Dachen.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ ETen.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ FakeSeigyou.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ IBM.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout || true

View File

@ -0,0 +1,60 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import SwiftUI
@main
struct vChewingInstallerApp: App {
var body: some Scene {
WindowGroup {
ZStack(alignment: .center) {
LinearGradient(
gradient: Gradient(
colors: [
Color(red: 0, green: 0, blue: 0xF4 / 255),
.black,
]
),
startPoint: .top, endPoint: .bottom
).overlay(alignment: .topLeading) {
Text("vChewing Input Method")
.font(.system(size: 30))
.italic().bold()
.padding()
.foregroundStyle(Color.white)
.shadow(color: .black, radius: 0, x: 5, y: 5)
}
MainView()
.shadow(color: .black, radius: 3, x: 0, y: 0)
}.frame(width: 1000, height: 630)
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
NSApp.windows.forEach { window in
window.titlebarAppearsTransparent = true
window.setContentSize(.init(width: 1000, height: 630))
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
window.styleMask.remove(.resizable)
window.orderFront(self)
}
}
.onDisappear {
NSApp.terminate(self)
}
}
.commands {
CommandGroup(replacing: .newItem) {}
CommandGroup(replacing: .appInfo) {}
CommandGroup(replacing: .help) {}
CommandGroup(replacing: .appVisibility) {}
CommandGroup(replacing: .systemServices) {}
}
}
}

View File

@ -0,0 +1,33 @@
#!/usr/bin/env swift
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
let strDataPath = "./"
func handleFiles(_ handler: @escaping ((url: URL, fileName: String)) -> Void) {
let rawURLs = FileManager.default.enumerator(at: URL(fileURLWithPath: strDataPath), includingPropertiesForKeys: nil)?.compactMap { $0 as? URL }
rawURLs?.forEach { url in
guard let fileName = url.pathComponents.last, fileName.lowercased() == "localizable.strings" else { return }
handler((url, fileName))
}
}
handleFiles { url, fileName in
guard let rawStr = try? String(contentsOf: url, encoding: .utf8) else { return }
let locale = Locale(identifier: "zh@collation=stroke")
do {
try rawStr.components(separatedBy: .newlines).filter { !$0.isEmpty }.sorted {
$0.compare($1, locale: locale) == .orderedAscending
}.joined(separator: "\n").description.appending("\n").write(to: url, atomically: false, encoding: .utf8)
} catch {
print("!! Error writing to \(fileName)")
}
}

View File

@ -11,6 +11,24 @@ BUILD_SETTINGS += ARCHS="$(ARCHS)"
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
endif
spmDebug:
swift build -c debug --package-path ./Packages/vChewing_MainAssembly/
spmRelease:
swift build -c release --package-path ./Packages/vChewing_MainAssembly/
spmLintFormat:
make lint --file=./Packages/Makefile || true
make format --file=./Packages/Makefile || true
spmClean:
@for currentDir in $$(ls ./Packages/); do \
if [ -d $$a ]; then \
echo "processing folder $$currentDir"; \
swift package clean --package-path ./Packages/$$currentDir || true; \
fi; \
done;
release:
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
@ -42,6 +60,7 @@ install-release: permission-check
.PHONY: clean
clean:
make clean --file=./Packages/Makefile || true
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
make clean --file=./Source/Data/Makefile || true

1
Packages/.clang-format Normal file
View File

@ -0,0 +1 @@
BasedOnStyle: Microsoft

104
Packages/.swiftformat.json Normal file
View File

@ -0,0 +1,104 @@
# SwiftFormat config compliant with Google Swift Guideline
# https://google.github.io/swift/#control-flow-statements
# Specify version used in a project
--swiftversion 5.5
# Rules explicitly required by the guideline
--rules \
blankLinesAroundMark, \
blankLinesAtEndOfScope, \
blankLinesAtStartOfScope, \
blankLinesBetweenScopes, \
braces, \
consecutiveBlankLines, \
consecutiveSpaces, \
duplicateImports, \
elseOnSameLine, \
emptyBraces, \
enumNamespaces, \
extensionAccessControl, \
hoistPatternLet, \
indent, \
leadingDelimiters, \
linebreakAtEndOfFile, \
markTypes, \
organizeDeclarations, \
redundantInit, \
redundantParens, \
redundantPattern, \
redundantRawValues, \
redundantType, \
redundantVoidReturnType, \
semicolons, \
sortedImports, \
sortedSwitchCases, \
spaceAroundBraces, \
spaceAroundBrackets, \
spaceAroundComments, \
spaceAroundGenerics, \
spaceAroundOperators, \
spaceAroundParens, \
spaceInsideBraces, \
spaceInsideBrackets, \
spaceInsideComments, \
spaceInsideGenerics, \
spaceInsideParens, \
todos, \
trailingClosures, \
trailingCommas, \
trailingSpace, \
typeSugar, \
void, \
wrap, \
wrapArguments, \
wrapAttributes, \
#
#
# Additional rules not mentioned in the guideline, but helping to keep the codebase clean
# Quoting the guideline:
# Common themes among the rules in this section are:
# avoid redundancy, avoid ambiguity, and prefer implicitness over explicitness
# unless being explicit improves readability and/or reduces ambiguity.
#
#
andOperator, \
isEmpty, \
redundantBackticks, \
redundantBreak, \
redundantExtensionACL, \
redundantGet, \
redundantLetError, \
redundantNilInit, \
redundantObjc, \
redundantReturn, \
redundantSelf, \
strongifiedSelf
# Options for basic rules
--extensionacl on-declarations
--funcattributes prev-line
--indent 2
--maxwidth 100
--typeattributes prev-line
--varattributes prev-line
--voidtype tuple
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapreturntype if-multiline
--wrapconditions after-first
# Option for additional rules
--self init-only
# Excluded folders
--exclude Pods,**/UNTESTED_TODO,vendor,fastlane
# https://github.com/NoemiRozpara/Google-SwiftFormat-Config

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "FolderMonitor",
platforms: [
.macOS(.v10_11),
.macOS(.v11),
],
products: [
.library(

View File

@ -38,14 +38,15 @@ public class FolderMonitor {
)
// Define the block to call when a file change is detected.
folderMonitorSource?.setEventHandler { [weak self] in
self?.folderDidChange?()
guard let self = self else { return }
self.folderDidChange?()
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
folderMonitorSource?.setCancelHandler { [weak self] in
guard let strongSelf = self else { return }
close(strongSelf.monitoredFolderFileDescriptor)
strongSelf.monitoredFolderFileDescriptor = -1
strongSelf.folderMonitorSource = nil
guard let self = self else { return }
close(self.monitoredFolderFileDescriptor)
self.monitoredFolderFileDescriptor = -1
self.folderMonitorSource = nil
}
// Start monitoring the directory via the source.
folderMonitorSource?.resume()

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "NSAttributedTextView",
platforms: [
.macOS(.v10_11),
.macOS(.v11),
],
products: [
.library(
@ -13,14 +13,18 @@ let package = Package(
),
],
dependencies: [
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
],
targets: [
.target(
name: "NSAttributedTextView",
dependencies: [
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
]
),
.testTarget(
name: "NSAttributedTextViewTests",
dependencies: ["NSAttributedTextView"]
),
]
)

View File

@ -6,7 +6,7 @@
// Modified by The vChewing Project in order to use it with AppKit.
import AppKit
import CocoaExtension
import OSFrameworkImpl
import SwiftUI
@available(macOS 10.15, *)
@ -72,6 +72,18 @@ public class NSAttributedTextView: NSView {
}
}
public init() {
super.init(frame: .zero)
#if compiler(>=5.9) && canImport(AppKit, _version: "14.0")
clipsToBounds = true // View
#endif
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributedStringValue(areaCalculation: Bool = false) -> NSAttributedString {
var newAttributes = attributes
let isVertical: Bool = !(direction == .horizontal)
@ -99,6 +111,7 @@ public class NSAttributedTextView: NSView {
public var backgroundColor: NSColor = .controlBackgroundColor
public var attributes: [NSAttributedString.Key: Any] = [
.kern: 0,
.verticalGlyphForm: true,
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
.foregroundColor: NSColor.textColor,
@ -130,8 +143,7 @@ public class NSAttributedTextView: NSView {
}
override public func draw(_ rect: CGRect) {
let context = NSGraphicsContext.current?.cgContext
guard let context = context else { return }
guard let currentNSGraphicsContext = NSGraphicsContext.current else { return }
let setter = CTFramesetterCreateWithAttributedString(attributedStringValue())
let path = CGPath(rect: rect, transform: nil)
let theCTFrameProgression: CTFrameProgression = {
@ -151,7 +163,16 @@ public class NSAttributedTextView: NSView {
let bgPath: NSBezierPath = .init(roundedRect: rect, xRadius: 0, yRadius: 0)
bgPath.fill()
currentRect = rect
CTFrameDraw(newFrame, context)
if #unavailable(macOS 10.10) {
// NSGraphicsContext.current?.cgContext macOS 10.10 Yosemite
//
let contextPtr: Unmanaged<CGContext>? = Unmanaged.fromOpaque(currentNSGraphicsContext.graphicsPort)
let theContext: CGContext? = contextPtr?.takeUnretainedValue()
guard let theContext = theContext else { return }
CTFrameDraw(newFrame, theContext)
} else {
CTFrameDraw(newFrame, currentNSGraphicsContext.cgContext)
}
}
}

View File

@ -0,0 +1,62 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
@testable import NSAttributedTextView
import OSFrameworkImpl
import Shared
import XCTest
class MainAssemblyTests: XCTestCase {
func testView() throws {
let testCtl: testController = .init()
var rect = testCtl.attrView.shrinkFrame()
var bigRect = rect
bigRect.size.width += NSFont.systemFontSize
bigRect.size.height += NSFont.systemFontSize
rect.origin.x += ceil(NSFont.systemFontSize / 2)
rect.origin.y += ceil(NSFont.systemFontSize / 2)
testCtl.attrView.frame = rect
testCtl.window?.setFrame(bigRect, display: true)
testCtl.window?.orderFront(nil)
testCtl.attrView.draw(testCtl.attrView.frame)
testCtl.window?.setIsVisible(true)
}
}
class testController: NSWindowController {
var attrView: NSAttributedTextView
init() {
let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0)
let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel]
let panel = NSPanel(
contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false
)
panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 2)
panel.hasShadow = true
panel.backgroundColor = NSColor.clear
panel.isOpaque = false
panel.isMovable = false
panel.contentView?.wantsLayer = true
panel.contentView?.layer?.cornerRadius = 7
panel.contentView?.layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor
attrView = NSAttributedTextView()
attrView.backgroundColor = NSColor.clear
attrView.textColor = NSColor.textColor
attrView.needsDisplay = true
attrView.text = "114514"
panel.contentView?.addSubview(attrView)
super.init(window: panel)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,4 @@
// Ref: https://stackoverflow.com/a/75870807/4162914
#import <IOKit/IOKitLib.h>
#import <IOKit/hid/IOHIDBase.h>

View File

@ -0,0 +1,33 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SwiftyCapsLockToggler",
platforms: [
.macOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "SwiftyCapsLockToggler",
targets: ["SwiftyCapsLockToggler"]
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "CapsLockToggler",
path: "Framework",
cSettings: [
.headerSearchPath("include"),
]
),
.target(
name: "SwiftyCapsLockToggler",
dependencies: ["CapsLockToggler"]
),
]
)

View File

@ -0,0 +1,64 @@
// Ref: https://stackoverflow.com/a/75870807/4162914
// #import <IOKit/IOKitLib.h>
// #import <IOKit/hid/IOHIDBase.h>
import CapsLockToggler
public enum CapsLockToggler {
public static func toggle() {
try? IOKit.handleHIDSystemService { ioConnect in
var state = false
IOHIDGetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), &state)
state.toggle()
IOHIDSetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), state)
}
}
public static var isOn: Bool {
var state = false
try? IOKit.handleHIDSystemService { ioConnect in
IOHIDGetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), &state)
}
return state
}
public static func turnOff() {
try? IOKit.handleHIDSystemService { ioConnect in
IOHIDSetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), false)
}
}
}
// Refactored by Shiki Suen (MIT License)
public enum IOKit {
public static func handleHIDSystemService(_ taskHandler: @escaping (io_connect_t) -> Void) throws {
let ioService: io_service_t = IOServiceGetMatchingService(0, IOServiceMatching(kIOHIDSystemClass))
var connect: io_connect_t = 0
let x = IOServiceOpen(ioService, mach_task_self_, UInt32(kIOHIDParamConnectType), &connect)
if let errorOne = Mach.KernReturn(rawValue: x), errorOne != .success {
throw errorOne
}
taskHandler(connect)
let y = IOServiceClose(connect)
if let errorTwo = Mach.KernReturn(rawValue: y), errorTwo != .success {
throw errorTwo
}
}
}
// Refactored by Shiki Suen (MIT License)
public enum Mach {
public enum KernReturn: Int32, Error {
case success = 0
case invalidAddress = 1
case protectionFailure = 2
case noSpace = 3
case invalidArgument = 4
case failure = 5
case resourceShortage = 6
case notReceiver = 7
case noAccess = 8
case memoryFailure = 9
}
}

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "BookmarkManager",
platforms: [
.macOS(.v10_11),
.macOS(.v10_13),
],
products: [
.library(

View File

@ -14,14 +14,17 @@ public class BookmarkManager {
return
}
if #available(macOS 10.13, *) {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
try data.write(to: bookmarkURL)
NSLog("Did save data to url")
} catch {
NSLog("Couldn't save bookmarks")
do {
var data: Data?
if #unavailable(macOS 10.13) {
data = NSKeyedArchiver.archivedData(withRootObject: bookmarkDic)
} else {
data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
}
try data?.write(to: bookmarkURL)
NSLog("Did save data to url")
} catch {
NSLog("Couldn't save bookmarks")
}
}
@ -34,9 +37,23 @@ public class BookmarkManager {
if fileExists(url) {
do {
let fileData = try Data(contentsOf: url)
if let fileBookmarks = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
if #available(macOS 11.0, *) {
if let fileBookmarks = try NSKeyedUnarchiver.unarchivedDictionary(ofKeyClass: NSURL.self, objectClass: NSData.self, from: fileData) as [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
} else if #available(macOS 10.11, *) {
if let fileBookmarks = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
} else {
if let fileBookmarks = NSKeyedUnarchiver.unarchiveObject(with: fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
}
} catch {
@ -82,12 +99,7 @@ public class BookmarkManager {
}
private func getBookmarkURL() -> URL? {
let urls = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
if let appSupportURL = urls.last {
let url = appSupportURL.appendingPathComponent("Bookmarks.dict")
return url
}
return nil
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last?.appendingPathComponent("Bookmarks.dict")
}
private func fileExists(_ url: URL) -> Bool {

29
Packages/Makefile Normal file
View File

@ -0,0 +1,29 @@
+.PHONY: all
all: debug
debug:
swift build -c debug --package-path ./vChewing_MainAssembly/
release:
swift build -c release --package-path ./vChewing_MainAssembly/
clean:
@for currentDir in $$(ls ./); do \
if [ -d $$a ]; then \
echo "processing folder $$currentDir"; \
swift package clean --package-path ./$$currentDir || true; \
fi; \
done;
.PHONY: lint format
lintFormat: lint format
format:
@swiftformat --swiftversion 5.5 --indent 2 ./
lint:
@git ls-files --exclude-standard | grep -E '\.swift$$' | swiftlint --fix --autocorrect
.PHONY: permission-check install-debug install-release

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "ShiftKeyUpChecker",
platforms: [
.macOS(.v10_11),
.macOS(.v11),
],
products: [
.library(

View File

@ -3,7 +3,6 @@
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import AppKit
import Carbon
private extension Date {
static func - (lhs: Date, rhs: Date) -> TimeInterval {
@ -12,6 +11,8 @@ private extension Date {
}
public struct ShiftKeyUpChecker {
// MARK: -
public init(useLShift: Bool = false, useRShift: Bool = false) {
toggleWithLShift = useLShift
toggleWithRShift = useRShift
@ -24,7 +25,7 @@ public struct ShiftKeyUpChecker {
public var enabled: Bool { toggleWithLShift || toggleWithRShift }
private var checkModifier: NSEvent.ModifierFlags { NSEvent.ModifierFlags.shift }
private var checkModifier: NSEvent.ModifierFlags { .shift }
private var checkKeyCode: [UInt16] {
var result = [UInt16]()
if toggleWithLShift { result.append(lShiftKeyCode) }
@ -32,46 +33,31 @@ public struct ShiftKeyUpChecker {
return result
}
private let delayInterval = 0.3
// MARK: -
///
private let delayInterval = 0.2
private var previousKeyCode: UInt16?
private var lastTime: Date = .init()
private var shiftIsBeingPressed = false
private mutating func checkModifierKeyUp(event: NSEvent) -> Bool {
guard checkKeyCode.contains(event.keyCode) else { return false }
if event.type == .flagsChanged,
event.modifierFlags.intersection(.deviceIndependentFlagsMask) == .init(rawValue: 0),
Date() - lastTime <= delayInterval, shiftIsBeingPressed
{
// modifier keyup event
lastTime = Date(timeInterval: -3600 * 4, since: Date())
return true
}
return false
private mutating func registerModifierKeyDown(event: NSEvent) {
var isKeyDown: Bool = event.type == .flagsChanged
// ModifierFlags OptionSet使 contains true false
isKeyDown = isKeyDown && event.modifierFlags.intersection(.deviceIndependentFlagsMask) == checkModifier
isKeyDown = isKeyDown && checkKeyCode.contains(event.keyCode)
lastTime = isKeyDown ? .init() : .init(timeInterval: .infinity * -1, since: Date())
previousKeyCode = isKeyDown ? event.keyCode : nil
}
private mutating func checkModifierKeyDown(event: NSEvent) -> Bool {
let isLeftShift = event.modifierFlags.rawValue & UInt(NX_DEVICELSHIFTKEYMASK) != 0
let isRightShift = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0
print("isLeftShift: \(isLeftShift), isRightShift: \(isRightShift)")
let isKeyDown =
event.type == .flagsChanged
&& checkModifier.contains(event.modifierFlags.intersection(.deviceIndependentFlagsMask))
&& checkKeyCode.contains(event.keyCode)
if isKeyDown {
// modifier keydown event
lastTime = Date()
if event.modifierFlags == .shift { shiftIsBeingPressed = true }
} else {
lastTime = Date(timeInterval: -3600 * 4, since: Date())
shiftIsBeingPressed = false
}
return false
}
// To confirm that the shift key is "pressed-and-released".
// To confirm that only the shift key is "pressed-and-released".
public mutating func check(_ event: NSEvent) -> Bool {
checkModifierKeyUp(event: event) || checkModifierKeyDown(event: event)
var met: Bool = event.type == .flagsChanged
met = met && checkKeyCode.contains(event.keyCode)
met = met && event.keyCode == previousKeyCode // KeyCode
met = met && event.modifierFlags.intersection(.deviceIndependentFlagsMask).isEmpty
met = met && Date() - lastTime <= delayInterval
_ = met ? lastTime = Date(timeInterval: .infinity * -1, since: Date()) : registerModifierKeyDown(event: event)
return met
}
}

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "LineReader",
platforms: [
.macOS(.v10_11),
.macOS(.v11),
],
products: [
.library(

View File

@ -1,7 +1,6 @@
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
import Foundation
import SwiftExtension
public class LineReader {
let encoding: String.Encoding

View File

@ -1,5 +0,0 @@
version: 1.8.2
builder:
configs:
- platform: ios
documentation_targets: [SwiftUIBackports]

View File

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2021 Shaps Benkau
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,22 +0,0 @@
// swift-tools-version: 5.5
import PackageDescription
let package = Package(
name: "SwiftUIBackports",
platforms: [
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macOS(.v10_11),
],
products: [
.library(
name: "SwiftUIBackports",
targets: ["SwiftUIBackports"]
),
],
targets: [
.target(name: "SwiftUIBackports"),
],
swiftLanguageVersions: [.v5]
)

View File

@ -1,131 +0,0 @@
## NOTICE
- This package copy is clang-formatted according to vChewing's clang-format style, with removal of certain dependencies / features not-required by vChewing.
- **If you want to use this package**, you might want to consult its original repository: https://github.com/shaps80/SwiftUIBackports
![watchOS](https://img.shields.io/badge/watchOS-DE1F51)
![macOS](https://img.shields.io/badge/macOS-EE751F)
![tvOS](https://img.shields.io/badge/tvOS-00B9BB)
![ios](https://img.shields.io/badge/iOS-0C62C7)
[![swift](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fshaps80%2FSwiftUIBackports%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/shaps80/SwiftUIBackports)
# SwiftUI Backports
Introducing a collection of SwiftUI backports to make your iOS development easier.
Many backports support iOS 13+ but where UIKIt features were introduced in later versions, the same will be applicable to these backports, to keep parity with UIKit.
In some cases, I've also included additional APIs that bring more features to your SwiftUI development.
> Note, **all** backports will be API-matching to Apple's offical APIs, any additional features will be provided separately.
All backports are fully documented, in most cases using Apple's own documentation for consistency. Please refer to the header docs or Apple's original documentation for more details.
There is also a [Demo project](https://github.com/shaps80/SwiftUIBackportsDemo) available where you can see full demonstrations of all backports and additional features, including reference code to help you get started.
> Lastly, I hope this repo also serves as a great resource for _how_ you can backport effectively with minimal hacks 👍
## Sponsor
Building useful libraries like these, takes time away from my family. I build these tools in my spare time because I feel its important to give back to the community. Please consider [Sponsoring](https://github.com/sponsors/shaps80) me as it helps keep me working on useful libraries like these 😬
You can also give me a follow and a 'thanks' anytime.
[![Twitter](https://img.shields.io/badge/Twitter-@shaps-4AC71B)](http://twitter.com/shaps)
## Usage
The library adopts a backport design by [Dave DeLong](https://davedelong.com/blog/2021/10/09/simplifying-backwards-compatibility-in-swift/) that makes use of a single type to improve discoverability and maintainability when the time comes to remove your backport implementations, in favour of official APIs.
Backports of pure types, can easily be discovered under the `Backport` namespace. Similarly, modifiers are discoverable under the `.backport` namespace.
> Unfortuantely `Environment` backports cannot be access this way, in those cases the Apple API values will be prefixed with `backport` to simplify discovery.
Types:
```swift
@Backport.AppStorage("filter-enabled")
private var filterEnabled: Bool = false
```
Modifier:
```swift
Button("Show Prompt") {
showPrompt = true
}
.sheet(isPresented: $showPrompt) {
Prompt()
.backport.presentationDetents([.medium, .large])
}
```
Environment:
```swift
@Environment(\.backportRefresh) private var refreshAction
```
## Backports
**SwiftUI**
- `asyncImage`
- `AppStorage`
- `background` ViewBuilder API
- `DismissAction`
- `DynamicTypeSize`
`Label`
`LabeledContent`
- `NavigationDestination` uses a standard NavigationView
- `navigationTitle` newer API
- `overlay` ViewBuilder API
- `onChange`
- `openURL`
- `ProgressView`
- `presentationDetents`
- `presentationDragIndicator`
- `quicklookPreview`
- `requestReview`
- `Refreshable` includes pull-to-refresh 
- `ScaledMetric`
- `StateObject`
- `scrollDisabled`
- `scrollDismissesKeyboard`
- `scrollIndicators`
- `Section(_ header:)`
- `task` async/await modifier
**UIKit**
- `UIHostingConfiguration` simplifies embedding SwiftUI in `UICollectionViewCell` and `UITableViewCell`
## Extras
**Modal Presentations**
Adding this to your presented view, you can use the provided closure to present an `ActionSheet` to a user when they attempt to dismiss interactively. You can also use this to disable interactive dismissals entirely.
```swift
presentation(isModal: true) { /* attempt */ }
```
**FittingGeometryReader**
A custom `GeometryReader` implementation that correctly auto-sizes itself to its content. This is useful in many cases where you need a `GeometryReader` but don't want it to implicitly take up its parent View's bounds.
**FittingScrollView**
A custom `ScrollView` that respects `Spacer`'s when the content is not scrollable. This is useful when you need to place a view at the edges of your scrollview while its content is small enough to not require scrolling. Another great use case is vertically centered content that becomes `top` aligned once the content requires scrolling.
**PageView**
A pure SwiftUI implementation of a page-based view, using the native `TabView` and my custom `FittingGeometryReader` to size itself correctly. Since this uses a `TabView` under-the-hood, this allows you to use the same APIs and features from that view.
## Installation
You can install manually (by copying the files in the `Sources` directory) or using Swift Package Manager (**preferred**)
To install using Swift Package Manager, add this to the `dependencies` section of your `Package.swift` file:
`.package(url: "https://github.com/shaps80/SwiftUIBackports.git", .upToNextMinor(from: "1.0.0"))`

View File

@ -1,62 +0,0 @@
import ObjectiveC
import SwiftUI
@available(macOS 10.15, *)
/// Provides a convenient method for backporting API,
/// including types, functions, properties, property wrappers and more.
///
/// To backport a SwiftUI Label for example, you could apply the
/// following extension:
///
/// extension Backport where Content == Any {
/// public struct Label<Title, Icon> { }
/// }
///
/// Now if we want to provide further extensions to our backport type,
/// we need to ensure we retain the `Content == Any` generic requirement:
///
/// extension Backport.Label where Content == Any, Title == Text, Icon == Image {
/// public init<S: StringProtocol>(_ title: S, systemName: String) { }
/// }
///
/// In addition to types, we can also provide backports for properties
/// and methods:
///
/// extension Backport.Label where Content: View {
/// func onChange<Value: Equatable>(of value: Value, perform action: (Value) -> Void) -> some View {
/// // `content` provides access to the extended type
/// content.modifier(OnChangeModifier(value, action))
/// }
/// }
///
public struct Backport<Wrapped> {
/// The underlying content this backport represents.
public let content: Wrapped
@available(macOS 10.15, *)
/// Initializes a new Backport for the specified content.
/// - Parameter content: The content (type) that's being backported
public init(_ content: Wrapped) {
self.content = content
}
}
@available(macOS 10.15, *)
public extension View {
/// Wraps a SwiftUI `View` that can be extended to provide backport functionality.
var backport: Backport<Self> { .init(self) }
}
@available(macOS 10.15, *)
public extension NSObjectProtocol {
/// Wraps an `NSObject` that can be extended to provide backport functionality.
var backport: Backport<Self> { .init(self) }
}
@available(macOS 10.15, *)
public extension AnyTransition {
/// Wraps an `AnyTransition` that can be extended to provide backport functionality.
static var backport: Backport<AnyTransition> {
Backport(.identity)
}
}

View File

@ -1,52 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
/// A geometry reader that automatically sizes its height to 'fit' its content.
public struct FittingGeometryReader<Content>: View where Content: View {
@State private var height: CGFloat = 10 // must be non-zero
private var content: (GeometryProxy) -> Content
@available(macOS 10.15, *)
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
self.content = content
}
@available(macOS 10.15, *)
public var body: some View {
GeometryReader { geo in
content(geo)
.fixedSize(horizontal: false, vertical: true)
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) {
height = $0.height
}
}
.frame(height: height)
}
}
@available(macOS 10.15, *)
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
@available(macOS 10.15, *)
private struct SizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.overlay(
GeometryReader { geo in
Color.clear.preference(
key: SizePreferenceKey.self,
value: geo.size
)
}
)
}
}

View File

@ -1,37 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
/// A scrollview that behaves more similarly to a `VStack` when its content size is small enough.
public struct FittingScrollView<Content: View>: View {
private let content: Content
private let showsIndicators: Bool
@available(macOS 10.15, *)
/// A new scrollview
/// - Parameters:
/// - showsIndicators: If true, the scroll view will show indicators when necessary
/// - content: The content for this scroll view
public init(showsIndicators: Bool = true, @ViewBuilder content: () -> Content) {
self.showsIndicators = showsIndicators
self.content = content()
}
@available(macOS 10.15, *)
public var body: some View {
GeometryReader { geo in
SwiftUI.ScrollView(showsIndicators: showsIndicators) {
VStack(spacing: 10) {
content
}
.frame(
maxWidth: geo.size.width,
minHeight: geo.size.height
)
}
}
}
}

View File

@ -1,47 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(tvOS, deprecated: 13)
@available(macOS, deprecated: 10.15)
@available(watchOS, deprecated: 6)
extension View {
/// Sets whether this presentation should act as a `modal`, preventing interactive dismissals
/// - Parameter isModal: If `true` the user will not be able to interactively dismiss
@ViewBuilder
@available(iOS, deprecated: 13, renamed: "backport.interactiveDismissDisabled(_:)")
public func presentation(isModal: Bool) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
backport.interactiveDismissDisabled(isModal)
} else {
self
}
#else
self
#endif
}
@available(macOS 10.15, *)
/// Provides fine-grained control over the dismissal.
/// - Parameters:
/// - isModal: If `true`, the user will not be able to interactively dismiss
/// - onAttempt: A closure that will be called when an interactive dismiss attempt occurs.
/// You can use this as an opportunity to present an ActionSheet to prompt the user.
@ViewBuilder
@available(iOS, deprecated: 13, renamed: "backport.interactiveDismissDisabled(_:onAttempt:)")
public func presentation(isModal: Bool = true, _ onAttempt: @escaping () -> Void) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
backport.interactiveDismissDisabled(isModal, onAttempt: onAttempt)
} else {
self
}
#else
self
#endif
}
}

View File

@ -1,152 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
private extension EnvironmentValues {
func containsValue(forKey key: String) -> Bool {
value(forKey: key) != nil
}
func value<T>(forKey key: String, from mirror: Mirror, as _: T.Type) -> T? {
// Found a match
if let value = mirror.descendant("value", "some") {
if let typedValue = value as? T {
print("Found value")
return typedValue
} else {
print(
"Value for key '\(key)' in the environment is of type '\(type(of: value))', but we expected '\(String(describing: T.self))'."
)
}
} else {
print(
"Found key '\(key)' in the environment, but it doesn't have the expected structure. The type hierarchy may have changed in your SwiftUI version."
)
}
return nil
}
/// Extracts a value from the environment by the name of its associated EnvironmentKey.
/// Can be used to grab private environment values such as foregroundColor ("ForegroundColorKey").
func value<T>(forKey key: String, as _: T.Type) -> T? {
if let mirror = value(forKey: key) as? Mirror {
return value(forKey: key, from: mirror, as: T.self)
} else if let value = value(forKey: key) as? T {
return value
} else {
return nil
}
}
func value(forKey key: String) -> Any? {
func keyFromTypeName(typeName: String) -> String? {
let expectedPrefix = "TypedElement<EnvironmentPropertyKey<"
guard typeName.hasPrefix(expectedPrefix) else {
print("Wrong prefix")
return nil
}
let rest = typeName.dropFirst(expectedPrefix.count)
let expectedSuffix = ">>"
guard rest.hasSuffix(expectedSuffix) else {
print("Wrong suffix")
return nil
}
let middle = rest.dropLast(expectedSuffix.count)
return String(middle)
}
/// `environmentMember` has type (for example) `TypedElement<EnvironmentPropertyKey<ForegroundColorKey>>`
/// TypedElement.value contains the value of the key.
func extract(startingAt environmentNode: Any) -> Any? {
let mirror = Mirror(reflecting: environmentNode)
let typeName = String(describing: type(of: environmentNode))
if let nodeKey = keyFromTypeName(typeName: typeName) {
if key == nodeKey {
return mirror
}
}
// Environment values are stored in a doubly linked list. The "before" and "after" keys point
// to the next environment member.
if let linkedListMirror = mirror.superclassMirror,
let nextNode = linkedListMirror.descendant("after", "some")
{
return extract(startingAt: nextNode)
}
return nil
}
let mirror = Mirror(reflecting: self)
if let firstEnvironmentValue = mirror.descendant("_plist", "elements", "some") {
if let node = extract(startingAt: firstEnvironmentValue) {
return node
} else {
return nil
}
} else {
return nil
}
}
}
@available(macOS 10.15, *)
@propertyWrapper
internal struct StringlyTypedEnvironment<Value> {
final class Store<StoredValue>: ObservableObject {
var value: StoredValue?
}
@Environment(\.self) private var env
@ObservedObject private var store = Store<Value>()
var key: String
init(key: String) {
self.key = key
}
private(set) var wrappedValue: Value? {
get { store.value }
nonmutating set { store.value = newValue }
}
}
@available(macOS 10.15, *)
extension StringlyTypedEnvironment: DynamicProperty {
func update() {
wrappedValue = env.value(forKey: key, as: Value.self)
}
}
@available(macOS 10.15, *)
@propertyWrapper
internal struct EnvironmentContains: DynamicProperty {
final class Store: ObservableObject {
var contains: Bool = false
}
@Environment(\.self) private var env
var key: String
@ObservedObject private var store = Store()
init(key: String) {
self.key = key
}
var wrappedValue: Bool {
get { store.contains }
nonmutating set { store.contains = newValue }
}
func update() {
wrappedValue = env.containsValue(forKey: key)
}
}

View File

@ -1,44 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
/*
The following code is for debugging purposes only!
*/
#if DEBUG
@available(macOS 10.15, *)
extension EnvironmentValues: CustomDebugStringConvertible {
public var debugDescription: String {
"\(self)"
.trimmingCharacters(in: .init(["[", "]"]))
.replacingOccurrences(of: "EnvironmentPropertyKey", with: "")
.replacingOccurrences(of: ", ", with: "\n")
}
}
@available(macOS 10.15, *)
struct EnvironmentOutputModifier: ViewModifier {
@Environment(\.self) private var environment
func body(content: Content) -> some View {
content
.onAppear {
print(environment.debugDescription)
}
}
}
@available(macOS 10.15, *)
extension View {
func printEnvironment() -> some View {
modifier(EnvironmentOutputModifier())
}
}
#endif

View File

@ -1,175 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(iOS)
internal typealias PlatformView = UIView
internal typealias PlatformViewController = UIViewController
#elseif os(macOS)
internal typealias PlatformView = NSView
internal typealias PlatformViewController = NSViewController
#endif
#if os(iOS) || os(macOS)
extension PlatformView {
func ancestor<ViewType: PlatformView>(ofType _: ViewType.Type) -> ViewType? {
var view = superview
while let s = view {
if let typed = s as? ViewType {
return typed
}
view = s.superview
}
return nil
}
var host: PlatformView? {
var view = superview
while let s = view {
if NSStringFromClass(type(of: s)).contains("ViewHost") {
return s
}
view = s.superview
}
return nil
}
func sibling<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
guard let superview = superview, let index = superview.subviews.firstIndex(of: self) else { return nil }
var views = superview.subviews
views.remove(at: index)
for subview in views.reversed() {
if let typed = subview.child(ofType: type) {
return typed
}
}
return nil
}
func child<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
for subview in subviews {
if let typed = subview as? ViewType {
return typed
} else if let typed = subview.child(ofType: type) {
return typed
}
}
return nil
}
}
internal struct Inspector {
var hostView: PlatformView
var sourceView: PlatformView
var sourceController: PlatformViewController
func ancestor<ViewType: PlatformView>(ofType _: ViewType.Type) -> ViewType? {
hostView.ancestor(ofType: ViewType.self)
}
func sibling<ViewType: PlatformView>(ofType _: ViewType.Type) -> ViewType? {
hostView.sibling(ofType: ViewType.self)
}
}
@available(macOS 10.15, *)
extension View {
private func inject<Wrapped>(_ content: Wrapped) -> some View where Wrapped: View {
overlay(content.frame(width: 0, height: 0))
}
func inspect<ViewType: PlatformView>(
selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void
) -> some View {
inject(InspectionView(selector: selector, customize: customize))
}
}
@available(macOS 10.15, *)
private struct InspectionView<ViewType: PlatformView>: View {
let selector: (Inspector) -> ViewType?
let customize: (ViewType) -> Void
var body: some View {
Representable(parent: self)
}
}
private class SourceView: PlatformView {
required init() {
super.init(frame: .zero)
isHidden = true
#if os(iOS)
isUserInteractionEnabled = false
#endif
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
#if os(iOS)
extension InspectionView {
struct Representable: UIViewRepresentable {
let parent: InspectionView
func makeUIView(context _: Context) -> UIView { .init() }
func updateUIView(_ view: UIView, context _: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController
?? view.window?.rootViewController
?? UIViewController()
)
guard let targetView = parent.selector(inspector) else { return }
parent.customize(targetView)
}
}
}
}
#elseif os(macOS)
@available(macOS 10.15, *)
extension InspectionView {
struct Representable: NSViewRepresentable {
let parent: InspectionView
func makeNSView(context _: Context) -> NSView {
.init(frame: .zero)
}
func updateNSView(_ view: NSView, context _: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController ?? NSViewController(nibName: nil, bundle: nil)
)
guard let targetView = parent.selector(inspector) else { return }
parent.customize(targetView)
}
}
}
}
#endif

View File

@ -1,40 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
#if os(iOS)
import UIKit
public extension UIView {
var parentController: UIViewController? {
var responder: UIResponder? = self
while !(responder is UIViewController), superview != nil {
if let next = responder?.next {
responder = next
}
}
return responder as? UIViewController
}
}
#endif
#if os(macOS)
import AppKit
@available(macOS 10.15, *)
public extension NSView {
var parentController: NSViewController? {
var responder: NSResponder? = self
while !(responder is NSViewController), superview != nil {
if let next = responder?.nextResponder {
responder = next
}
}
return responder as? NSViewController
}
}
#endif

View File

@ -1,47 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(iOS) || os(tvOS)
/*
Since UICollectionView is not designed to support SwiftUI out of the box,
we need to use a little trick to get the SwiftUI View's to ignore safeArea
insets, otherwise our cell's will not always layout correctly.
*/
extension UIHostingController {
convenience init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
} else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
.zero
}
class_addMethod(
viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets),
method_getTypeEncoding(method)
)
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
#endif

View File

@ -1,15 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
#if os(iOS)
import UIKit
extension UIApplication {
static var activeScene: UIWindowScene? {
shared.connectedScenes
.first { $0.activationState == .foregroundActive }
as? UIWindowScene
}
}
#endif

View File

@ -1,408 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A property wrapper type that reflects a value from `Store` and
/// invalidates a view on a change in value in that store.
@propertyWrapper
public struct AppStorage<Value>: DynamicProperty {
@ObservedObject
private var _value: RefStorage<Value>
private let commitHandler: (Value) -> Void
public var wrappedValue: Value {
get { _value.value }
nonmutating set {
commitHandler(newValue)
_value.value = newValue
}
}
public var projectedValue: Binding<Value> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
private init(
value: Value, store: UserDefaults, key: String, get: @escaping (Any?) -> Value?, set: @escaping (Value) -> Void
) {
_value = RefStorage(value: value, store: store, key: key, transform: get)
commitHandler = set
}
}
}
@available(macOS 10.15, *)
public extension Backport.AppStorage {
/// Creates a property that can read and write to a boolean user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a boolean value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Bool {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to an integer user default.
///
/// - Parameters:
/// - wrappedValue: The default value if an integer value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Int {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a double user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a double value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Double {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a string user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a string value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == String {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a [string] user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a string value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == [String] {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a [String: Bool] user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a string value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == [String: Bool] {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a url user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a url value is not specified for
/// the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == URL {
let value = store.url(forKey: key) ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { ($0 as? String).flatMap(URL.init) },
set: { store.set($0.absoluteString, forKey: key) }
)
}
/// Creates a property that can read and write to a user default as data.
///
/// Avoid storing large data blobs in store, such as image data,
/// as it can negatively affect performance of your app.
///
/// - Parameters:
/// - wrappedValue: The default value if a data value is not specified for
/// the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Data {
let value = store.value(forKey: key) as? Data ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
}
@available(macOS 10.15, *)
public extension Backport.AppStorage where Wrapped == Any, Value: ExpressibleByNilLiteral {
/// Creates a property that can read and write an Optional boolean user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Bool? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional integer user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Int? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional double user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Double? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional string user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == String? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional URL user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == URL? {
let value = store.url(forKey: key) ?? .none
self.init(
value: value, store: store, key: key,
get: { ($0 as? String).flatMap(URL.init) },
set: { store.set($0?.absoluteString, forKey: key) }
)
}
/// Creates a property that can read and write an Optional data user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Data? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
}
@available(macOS 10.15, *)
public extension Backport.AppStorage where Wrapped == Any, Value: RawRepresentable {
/// Creates a property that can read and write to a string user default,
/// transforming that to `RawRepresentable` data type.
///
/// A common usage is with enumerations:
///
/// enum MyEnum: String {
/// case a
/// case b
/// case c
/// }
///
/// @AppStorage("MyEnumValue") private var value = MyEnum.a
///
/// - Parameters:
/// - wrappedValue: The default value if a string value
/// is not specified for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == String {
let rawValue = store.value(forKey: key) as? Value.RawValue
let value = rawValue.flatMap(Value.init) ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.setValue($0.rawValue, forKey: key) }
)
}
/// Creates a property that can read and write to an integer user default,
/// transforming that to `RawRepresentable` data type.
///
/// A common usage is with enumerations:
///
/// enum MyEnum: Int {
/// case a
/// case b
/// case c
/// }
///
/// @AppStorage("MyEnumValue") private var value = MyEnum.a
///
/// - Parameters:
/// - wrappedValue: The default value if an integer value
/// is not specified for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == Int {
let rawValue = store.value(forKey: key) as? Value.RawValue
let value = rawValue.flatMap(Value.init) ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.setValue($0.rawValue, forKey: key) }
)
}
}
@available(macOS 10.15, *)
private final class RefStorage<Value>: NSObject, ObservableObject {
@Published
fileprivate var value: Value
private let defaultValue: Value
private let store: UserDefaults
private let key: String
private let transform: (Any?) -> Value?
deinit {
store.removeObserver(self, forKeyPath: key)
}
init(value: Value, store: UserDefaults, key: String, transform: @escaping (Any?) -> Value?) {
self.value = value
defaultValue = value
self.store = store
self.key = key
self.transform = transform
super.init()
store.addObserver(self, forKeyPath: key, options: .new, context: nil)
}
override func observeValue(
forKeyPath _: String?,
of _: Any?,
change: [NSKeyValueChangeKey: Any]?,
context _: UnsafeMutableRawPointer?
) {
value = change?[.newKey].flatMap(transform) ?? defaultValue
}
}

View File

@ -1,226 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 15.0)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// Loads and displays an image from the specified URL.
///
/// Until the image loads, SwiftUI displays a default placeholder. When
/// the load operation completes successfully, SwiftUI updates the
/// view to show the loaded image. If the operation fails, SwiftUI
/// continues to display the placeholder. The following example loads
/// and displays an icon from an example server:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png"))
///
/// If you want to customize the placeholder or apply image-specific
/// modifiers --- like ``Image/resizable(capInsets:resizingMode:)`` ---
/// to the loaded image, use the ``init(url:scale:content:placeholder:)``
/// initializer instead.
///
/// - Parameters:
/// - url: The URL of the image to display.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
@ViewBuilder
public static func asyncImage(url: URL?, scale: CGFloat = 1) -> some View {
_asyncImage(url: url, scale: scale)
}
/// Loads and displays a modifiable image from the specified URL using
/// a custom placeholder until the image loads.
///
/// Until the image loads, SwiftUI displays the placeholder view that
/// you specify. When the load operation completes successfully, SwiftUI
/// updates the view to show content that you specify, which you
/// create using the loaded image. For example, you can show a green
/// placeholder, followed by a tiled version of the loaded image:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png")) { image in
/// image.resizable(resizingMode: .tile)
/// } placeholder: {
/// Color.green
/// }
///
/// If the load operation fails, SwiftUI continues to display the
/// placeholder. To be able to display a different view on a load error,
/// use the ``init(url:scale:transaction:content:)`` initializer instead.
///
/// - Parameters:
/// - url: The URL of the image to display.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
/// - content: A closure that takes the loaded image as an input, and
/// returns the view to show. You can return the image directly, or
/// modify it as needed before returning it.
/// - placeholder: A closure that returns the view to show until the
/// load operation completes successfully.
@ViewBuilder
public static func asyncImage<I: View, P: View>(
url: URL?, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I,
@ViewBuilder placeholder: @escaping () -> P
) -> some View {
_asyncImage(url: url, scale: scale, content: content, placeholder: placeholder)
}
/// Loads and displays a modifiable image from the specified URL in phases.
///
/// If you set the asynchronous image's URL to `nil`, or after you set the
/// URL to a value but before the load operation completes, the phase is
/// ``asyncImagePhase/empty``. After the operation completes, the phase
/// becomes either ``asyncImagePhase/failure(_:)`` or
/// ``asyncImagePhase/success(_:)``. In the first case, the phase's
/// ``asyncImagePhase/error`` value indicates the reason for failure.
/// In the second case, the phase's ``asyncImagePhase/image`` property
/// contains the loaded image. Use the phase to drive the output of the
/// `content` closure, which defines the view's appearance:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
/// if let image = phase.image {
/// image // Displays the loaded image.
/// } else if phase.error != nil {
/// Color.red // Indicates an error.
/// } else {
/// Color.blue // Acts as a placeholder.
/// }
/// }
///
/// To add transitions when you change the URL, apply an identifier to the
/// ``asyncImage``.
///
/// - Parameters:
/// - url: The URL of the image to display.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
/// - transaction: The transaction to use when the phase changes.
/// - content: A closure that takes the load phase as an input, and
/// returns the view to display for the specified phase.
@ViewBuilder
public static func asyncImage<Content: View>(
url: URL?, scale: CGFloat = 1, transaction: Transaction = Transaction(),
@ViewBuilder content: @escaping (asyncImagePhase) -> Content
) -> some View {
_asyncImage(url: url, scale: scale, transaction: transaction, content: content)
}
/// The current phase of the asynchronous image loading operation.
///
/// When you create an ``asyncImage`` instance with the
/// ``asyncImage/init(url:scale:transaction:content:)`` initializer, you define
/// the appearance of the view using a `content` closure. SwiftUI calls the
/// closure with a phase value at different points during the load operation
/// to indicate the current state. Use the phase to decide what to draw.
/// For example, you can draw the loaded image if it exists, a view that
/// indicates an error, or a placeholder:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
/// if let image = phase.image {
/// image // Displays the loaded image.
/// } else if phase.error != nil {
/// Color.red // Indicates an error.
/// } else {
/// Color.blue // Acts as a placeholder.
/// }
/// }
public enum asyncImagePhase {
/// No image is loaded.
case empty
/// An image succesfully loaded.
case success(Image)
/// An image failed to load with an error.
case failure(Error)
/// The loaded image, if any.
public var image: Image? {
guard case let .success(image) = self else { return nil }
return image
}
/// The error that occurred when attempting to load an image, if any.
public var error: Error? {
guard case let .failure(error) = self else { return nil }
return error
}
}
// An iOS 13+ async/await backport implementation
private struct _asyncImage<Content: View>: View {
@State private var phase: asyncImagePhase = .empty
var url: URL?
var scale: CGFloat = 1
var transaction: Transaction = .init()
var content: (Backport<Any>.asyncImagePhase) -> Content
public var body: some View {
ZStack {
content(phase)
}
.backport.task(id: url) {
do {
guard !Task.isCancelled, let url = url else { return }
let (data, _) = try await URLSession.shared.backport.data(from: url)
guard !Task.isCancelled else { return }
#if os(macOS)
if let image = NSImage(data: data) {
withTransaction(transaction) {
phase = .success(Image(nsImage: image))
}
}
#else
if let image = UIImage(data: data, scale: scale) {
withTransaction(transaction) {
phase = .success(Image(uiImage: image))
}
}
#endif
} catch {
phase = .failure(error)
}
}
}
init(url: URL?, scale: CGFloat = 1) where Content == AnyView {
self.url = url
self.scale = scale
content = { AnyView($0.image) }
}
init<I, P>(
url: URL?, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I,
@ViewBuilder placeholder: @escaping () -> P
) where Content == _ConditionalContent<I, P> {
self.url = url
self.scale = scale
transaction = Transaction()
self.content = { phase -> _ConditionalContent<I, P> in
if let image = phase.image {
return ViewBuilder.buildEither(first: content(image))
} else {
return ViewBuilder.buildEither(second: placeholder())
}
}
}
init(
url: URL?, scale: CGFloat = 1, transaction: Transaction = Transaction(),
@ViewBuilder content: @escaping (Backport<Any>.asyncImagePhase) -> Content
) {
self.url = url
self.scale = scale
self.transaction = transaction
self.content = content
}
}
}

View File

@ -1,136 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Layers the views that you specify behind this view.
///
/// Use this modifier to place one or more views behind another view.
/// For example, you can place a collection of stars beind a ``Text`` view:
///
/// Text("ABCDEF")
/// .background(alignment: .leading) { Star(color: .red) }
/// .background(alignment: .center) { Star(color: .green) }
/// .background(alignment: .trailing) { Star(color: .blue) }
///
/// The example above assumes that you've defined a `Star` view with a
/// parameterized color:
///
/// struct Star: View {
/// var color: Color
///
/// var body: some View {
/// Image(systemName: "star.fill")
/// .foregroundStyle(color)
/// }
/// }
///
/// By setting different `alignment` values for each modifier, you make the
/// stars appear in different places behind the text:
///
/// ![A screenshot of the letters A, B, C, D, E, and F written in front of
/// three stars. The stars, from left to right, are red, green, and
/// blue.](View-background-2)
///
/// If you specify more than one view in the `content` closure, the modifier
/// collects all of the views in the closure into an implicit ``ZStack``,
/// taking them in order from back to front. For example, you can layer a
/// vertical bar behind a circle, with both of those behind a horizontal
/// bar:
///
/// Color.blue
/// .frame(width: 200, height: 10) // Creates a horizontal bar.
/// .background {
/// Color.green
/// .frame(width: 10, height: 100) // Creates a vertical bar.
/// Circle()
/// .frame(width: 50, height: 50)
/// }
///
/// Both the background modifier and the implicit ``ZStack`` composed from
/// the background content --- the circle and the vertical bar --- use a
/// default ``Alignment/center`` alignment. The vertical bar appears
/// centered behind the circle, and both appear as a composite view centered
/// behind the horizontal bar:
///
/// ![A screenshot of a circle with a horizontal blue bar layered on top
/// and a vertical green bar layered underneath. All of the items are center
/// aligned.](View-background-3)
///
/// If you specify an alignment for the background, it applies to the
/// implicit stack rather than to the individual views in the closure. You
/// can see this if you add the ``Alignment/leading`` alignment:
///
/// Color.blue
/// .frame(width: 200, height: 10)
/// .background(alignment: .leading) {
/// Color.green
/// .frame(width: 10, height: 100)
/// Circle()
/// .frame(width: 50, height: 50)
/// }
///
/// The vertical bar and the circle move as a unit to align the stack
/// with the leading edge of the horizontal bar, while the
/// vertical bar remains centered on the circle:
///
/// ![A screenshot of a horizontal blue bar in front of a circle, which
/// is in front of a vertical green bar. The horizontal bar and the circle
/// are center aligned with each other; the left edges of the circle
/// and the horizontal are aligned.](View-background-3a)
///
/// To control the placement of individual items inside the `content`
/// closure, either use a different background modifier for each item, as
/// the earlier example of stars under text demonstrates, or add an explicit
/// ``ZStack`` inside the content closure with its own alignment:
///
/// Color.blue
/// .frame(width: 200, height: 10)
/// .background(alignment: .leading) {
/// ZStack(alignment: .leading) {
/// Color.green
/// .frame(width: 10, height: 100)
/// Circle()
/// .frame(width: 50, height: 50)
/// }
/// }
///
/// The stack alignment ensures that the circle's leading edge aligns with
/// the vertical bar's, while the background modifier aligns the composite
/// view with the horizontal bar:
///
/// ![A screenshot of a horizontal blue bar in front of a circle, which
/// is in front of a vertical green bar. All items are aligned on their
/// left edges.](View-background-4)
///
/// You can achieve layering without a background modifier by putting both
/// the modified view and the background content into a ``ZStack``. This
/// produces a simpler view hierarchy, but it changes the layout priority
/// that SwiftUI applies to the views. Use the background modifier when you
/// want the modified view to dominate the layout.
///
/// If you want to specify a ``ShapeStyle`` like a
/// ``HierarchicalShapeStyle`` or a ``Material`` as the background, use
/// ``View/background(_:ignoresSafeAreaEdges:)`` instead.
/// To specify a ``Shape`` or ``InsettableShape``, use
/// ``View/background(_:in:fillStyle:)-89n7j`` or
/// ``View/background(_:in:fillStyle:)-20tq5``, respectively.
///
/// - Parameters:
/// - alignment: The alignment that the modifier uses to position the
/// implicit ``ZStack`` that groups the background views. The default
/// is ``Alignment/center``.
/// - content: A ``ViewBuilder`` that you use to declare the views to draw
/// behind this view, stacked in a cascading order from bottom to top.
/// The last view that you list appears at the front of the stack.
///
/// - Returns: A view that uses the specified content as a background.
func background<Content: View>(alignment: Alignment = .center, @ViewBuilder _ content: () -> Content)
-> some View
{
self.content.background(content(), alignment: alignment)
}
}

View File

@ -1,191 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension EnvironmentValues {
/// An action that dismisses the current presentation.
///
/// Use this environment value to get the ``Backport.DismissAction`` instance
/// for the current ``Environment``. Then call the instance
/// to perform the dismissal. You call the instance directly because
/// it defines a ``Backport.DismissAction/callAsFunction()``
/// method that Swift calls when you call the instance.
///
/// For example, you can create a button that calls the ``Backport.DismissAction``:
///
/// private struct SheetContents: View {
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Button("Done") {
/// dismiss()
/// }
/// }
/// }
///
/// If you present the `SheetContents` view in a sheet, the user can dismiss
/// the sheet by tapping or clicking the sheet's button:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// SheetContents()
/// }
/// }
/// }
///
/// Be sure that you define the action in the appropriate environment.
/// For example, don't reorganize the `DetailView` in the example above
/// so that it creates the `dismiss` property and calls it from the
/// ``View/sheet(item:onDismiss:content:)`` view modifier's `content`
/// closure:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
/// @Environment(\.backportDismiss) private var dismiss // Applies to DetailView.
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// Button("Done") {
/// dismiss() // Fails to dismiss the sheet.
/// }
/// }
/// }
/// }
///
/// If you do this, the sheet fails to dismiss because the action applies
/// to the environment where you declared it, which is that of the detail
/// view, rather than the sheet. In fact, if you've presented the detail
/// view in a ``NavigationView``, the dismissal pops the detail view
/// the navigation stack.
///
/// The dismiss action has no effect on a view that isn't currently
/// presented. If you need to query whether SwiftUI is currently presenting
/// a view, read the ``EnvironmentValues/backportIsPresented`` environment value.
var backportDismiss: Backport<Any>.DismissAction {
.init(presentation: presentationMode)
}
@available(macOS 10.15, *)
/// A Boolean value that indicates whether the view associated with this
/// environment is currently presented.
///
/// You can read this value like any of the other ``EnvironmentValues``
/// by creating a property with the ``Environment`` property wrapper:
///
/// @Environment(\.backportIsPresented) private var isPresented
///
/// Read the value inside a view if you need to know when SwiftUI
/// presents that view. For example, you can take an action when SwiftUI
/// presents a view by using the ``View/onChange(of:perform:)``
/// modifier:
///
/// .onChange(of: isPresented) { isPresented in
/// if isPresented {
/// // Do something when first presented.
/// }
/// }
///
/// This behaves differently than ``View/onAppear(perform:)``, which
/// SwiftUI can call more than once for a given presentation, like
/// when you navigate back to a view that's already in the
/// navigation hierarchy.
///
/// To dismiss the currently presented view, use
/// ``EnvironmentValues/backportDismiss``.
var backportIsPresented: Bool {
presentationMode.wrappedValue.isPresented
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped: Any {
/// An action that dismisses a presentation.
///
/// Use the ``EnvironmentValues/dismiss`` environment value to get the instance
/// of this structure for a given ``Environment``. Then call the instance
/// to perform the dismissal. You call the instance directly because
/// it defines a ``DismissAction/callAsFunction()``
/// method that Swift calls when you call the instance.
///
/// For example, you can create a button that calls the ``DismissAction``:
///
/// private struct SheetContents: View {
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Button("Done") {
/// dismiss()
/// }
/// }
/// }
///
/// If you present the `SheetContents` view in a sheet, the user can dismiss
/// the sheet by tapping or clicking the sheet's button:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// SheetContents()
/// }
/// }
/// }
///
/// Be sure that you define the action in the appropriate environment.
/// For example, don't reorganize the `DetailView` in the example above
/// so that it creates the `dismiss` property and calls it from the
/// ``View/sheet(item:onDismiss:content:)`` view modifier's `content`
/// closure:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
/// @Environment(\.backportDismiss) private var dismiss // Applies to DetailView.
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// Button("Done") {
/// dismiss() // Fails to dismiss the sheet.
/// }
/// }
/// }
/// }
///
/// If you do this, the sheet fails to dismiss because the action applies
/// to the environment where you declared it, which is that of the detail
/// view, rather than the sheet. In fact, if you've presented the detail
/// view in a ``NavigationView``, the dismissal pops the detail view
/// from the navigation stack.
///
/// The dismiss action has no effect on a view that isn't currently
/// presented. If you need to query whether SwiftUI is currently presenting
/// a view, read the ``EnvironmentValues/backportIsPresented`` environment value.
public struct DismissAction {
var presentation: Binding<PresentationMode>
public func callAsFunction() {
presentation.wrappedValue.dismiss()
}
}
}

View File

@ -1,64 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
private struct BackportDynamicTypeKey: EnvironmentKey {
static var defaultValue: Backport.DynamicTypeSize = .large
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
extension EnvironmentValues {
/// Sets the Dynamic Type size within the view to the given value.
///
/// As an example, you can set a Dynamic Type size in `ContentView` to be
/// ``DynamicTypeSize/xLarge`` (this can be useful in previews to see your
/// content at a different size) like this:
///
/// ContentView()
/// .backport.dynamicTypeSize(.xLarge)
///
/// If a Dynamic Type size range is applied after setting a value,
/// the value is limited by that range:
///
/// ContentView() // Dynamic Type size will be .large
/// .backport.dynamicTypeSize(...DynamicTypeSize.large)
/// .backport.dynamicTypeSize(.xLarge)
///
/// When limiting the Dynamic Type size, consider if adding a
/// large content view with ``View/accessibilityShowsLargeContentViewer()``
/// would be appropriate.
///
/// - Parameter size: The size to set for this view.
///
/// - Returns: A view that sets the Dynamic Type size to the specified
/// `size`.
public var backportDynamicTypeSize: Backport<Any>.DynamicTypeSize {
get { .init(self[keyPath: \.sizeCategory]) }
set { self[keyPath: \.sizeCategory] = newValue.sizeCategory }
}
}
@available(macOS 10.15, *)
private struct DynamicTypeRangeKey: EnvironmentKey {
static var defaultValue: Range<Backport<Any>.DynamicTypeSize> {
.init(uncheckedBounds: (lower: .xSmall, upper: .accessibility5))
}
}
@available(macOS 10.15, *)
extension EnvironmentValues {
var dynamicTypeRange: Range<Backport<Any>.DynamicTypeSize> {
get { self[DynamicTypeRangeKey.self] }
set {
let current = self[DynamicTypeRangeKey.self]
self[DynamicTypeRangeKey.self] = current.clamped(to: newValue)
}
}
}

View File

@ -1,116 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Sets the Dynamic Type size within the view to the given value.
///
/// As an example, you can set a Dynamic Type size in `ContentView` to be
/// ``DynamicTypeSize/xLarge`` (this can be useful in previews to see your
/// content at a different size) like this:
///
/// ContentView()
/// .dynamicTypeSize(.xLarge)
///
/// If a Dynamic Type size range is applied after setting a value,
/// the value is limited by that range:
///
/// ContentView() // Dynamic Type size will be .large
/// .dynamicTypeSize(...DynamicTypeSize.large)
/// .dynamicTypeSize(.xLarge)
///
/// When limiting the Dynamic Type size, consider if adding a
/// large content view with ``View/accessibilityShowsLargeContentViewer()``
/// would be appropriate.
///
/// - Parameter size: The size to set for this view.
///
/// - Returns: A view that sets the Dynamic Type size to the specified
/// `size`.
@ViewBuilder
func dynamicTypeSize(_ size: Backport<Any>.DynamicTypeSize) -> some View {
content.environment(\.backportDynamicTypeSize, size)
}
@available(macOS 10.15, *)
/// Limits the Dynamic Type size within the view to the given range.
///
/// As an example, you can constrain the maximum Dynamic Type size in
/// `ContentView` to be no larger than ``DynamicTypeSize/large``:
///
/// ContentView()
/// .dynamicTypeSize(...DynamicTypeSize.large)
///
/// If the Dynamic Type size is limited to multiple ranges, the result is
/// their intersection:
///
/// ContentView() // Dynamic Type sizes are from .small to .large
/// .dynamicTypeSize(.small...)
/// .dynamicTypeSize(...DynamicTypeSize.large)
///
/// A specific Dynamic Type size can still be set after a range is applied:
///
/// ContentView() // Dynamic Type size is .xLarge
/// .dynamicTypeSize(.xLarge)
/// .dynamicTypeSize(...DynamicTypeSize.large)
///
/// When limiting the Dynamic Type size, consider if adding a
/// large content view with ``View/accessibilityShowsLargeContentViewer()``
/// would be appropriate.
///
/// - Parameter range: The range of sizes that are allowed in this view.
///
/// - Returns: A view that constrains the Dynamic Type size of this view
/// within the specified `range`.
@ViewBuilder
func dynamicTypeSize<T>(_ range: T) -> some View
where T: RangeExpression, T.Bound == Backport<Any>.DynamicTypeSize
{
if let range = range as? Range<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, range)
} else if let range = range as? ClosedRange<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (lower: range.lowerBound, upper: range.upperBound)))
} else if let range = range as? PartialRangeFrom<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (range.lowerBound, .accessibility5)))
} else if let range = range as? PartialRangeUpTo<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (.xSmall, range.upperBound)))
} else if let range = range as? PartialRangeThrough<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (.xSmall, range.upperBound)))
} else {
content
.modifier(DynamicTypeRangeModifier())
}
}
}
@available(macOS 10.15, *)
private struct DynamicTypeRangeModifier: ViewModifier {
@Environment(\.dynamicTypeRange) private var range
@Environment(\.backportDynamicTypeSize) private var size
@available(macOS 10.15, *)
private var resolvedSize: Backport<Any>.DynamicTypeSize {
print(range)
return range.contains(size)
? size
: max(range.lowerBound, min(range.upperBound, size))
}
@available(macOS 10.15, *)
func body(content: Content) -> some View {
content.environment(\.backportDynamicTypeSize, resolvedSize)
}
}

View File

@ -1,225 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A Dynamic Type size, which specifies how large scalable content should be.
///
/// For more information about Dynamic Type sizes in iOS, see iOS Human Interface Guidelines >
/// [Dynamic Type Sizes](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#dynamic-type-sizes).
/// For more information about Dynamic Type sizes in watchOS, see watchOS Human Interface Guidelines >
/// [Dynamic Type Sizes](https://developer.apple.com/design/human-interface-guidelines/watchos/visual/typography/#dynamic-type-sizes).
public enum DynamicTypeSize: Hashable, Comparable, CaseIterable {
/// An extra small size.
case xSmall
/// A small size.
case small
/// A medium size.
case medium
/// A large size.
case large
/// An extra large size.
case xLarge
/// An extra extra large size.
case xxLarge
/// An extra extra extra large size.
case xxxLarge
/// The first accessibility size.
case accessibility1
/// The second accessibility size.
case accessibility2
/// The third accessibility size.
case accessibility3
/// The fourth accessibility size.
case accessibility4
/// The fifth accessibility size.
case accessibility5
/// A Boolean value indicating whether the size is one that is associated
/// with accessibility.
public var isAccessibilitySize: Bool {
self >= .accessibility1
}
#if os(iOS) || os(tvOS)
/// Create a Dynamic Type size from its `UIContentSizeCategory` equivalent.
public init?(_ uiSizeCategory: UIContentSizeCategory) {
switch uiSizeCategory {
case .extraSmall:
self = .xSmall
case .small:
self = .small
case .medium:
self = .medium
case .large:
self = .medium
case .extraLarge:
self = .xLarge
case .extraExtraLarge:
self = .xxLarge
case .extraExtraExtraLarge:
self = .xxxLarge
case .accessibilityMedium:
self = .accessibility1
case .accessibilityLarge:
self = .accessibility2
case .accessibilityExtraLarge:
self = .accessibility3
case .accessibilityExtraExtraLarge:
self = .accessibility4
case .accessibilityExtraExtraExtraLarge:
self = .accessibility5
default:
return nil
}
}
#endif
internal init(_ sizeCategory: ContentSizeCategory) {
switch sizeCategory {
case .extraSmall:
self = .xSmall
case .small:
self = .small
case .medium:
self = .medium
case .large:
self = .large
case .extraLarge:
self = .xLarge
case .extraExtraLarge:
self = .xxLarge
case .extraExtraExtraLarge:
self = .xxxLarge
case .accessibilityMedium:
self = .accessibility1
case .accessibilityLarge:
self = .accessibility2
case .accessibilityExtraLarge:
self = .accessibility3
case .accessibilityExtraExtraLarge:
self = .accessibility4
case .accessibilityExtraExtraExtraLarge:
self = .accessibility5
default:
self = .large
}
}
var sizeCategory: ContentSizeCategory {
switch self {
case .xSmall:
return .extraSmall
case .small:
return .small
case .medium:
return .medium
case .large:
return .large
case .xLarge:
return .extraLarge
case .xxLarge:
return .extraExtraLarge
case .xxxLarge:
return .extraExtraExtraLarge
case .accessibility1:
return .accessibilityMedium
case .accessibility2:
return .accessibilityLarge
case .accessibility3:
return .accessibilityExtraLarge
case .accessibility4:
return .accessibilityExtraExtraLarge
case .accessibility5:
return .accessibilityExtraExtraExtraLarge
}
}
}
}
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
extension Backport.DynamicTypeSize {
var dynamicTypeSize: DynamicTypeSize {
switch self {
case .xSmall:
return .xSmall
case .small:
return .small
case .medium:
return .medium
case .large:
return .large
case .xLarge:
return .xLarge
case .xxLarge:
return .xxLarge
case .xxxLarge:
return .xxxLarge
case .accessibility1:
return .accessibility1
case .accessibility2:
return .accessibility2
case .accessibility3:
return .accessibility3
case .accessibility4:
return .accessibility4
case .accessibility5:
return .accessibility5
}
}
}
#if os(iOS) || os(tvOS)
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
extension UIContentSizeCategory {
public init(_ dynamicTypeSize: Backport<Any>.DynamicTypeSize?) {
switch dynamicTypeSize {
case .xSmall:
self = .extraSmall
case .small:
self = .small
case .medium:
self = .medium
case .large:
self = .large
case .xLarge:
self = .extraLarge
case .xxLarge:
self = .extraExtraLarge
case .xxxLarge:
self = .extraExtraExtraLarge
case .accessibility1:
self = .accessibilityMedium
case .accessibility2:
self = .accessibilityLarge
case .accessibility3:
self = .accessibilityExtraLarge
case .accessibility4:
self = .accessibilityExtraExtraLarge
case .accessibility5:
self = .accessibilityExtraExtraExtraLarge
case .none:
self = .large
}
}
}
#endif

View File

@ -1,184 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A standard label for user interface items, consisting of an icon with a
/// title.
///
/// One of the most common and recognizable user interface components is the
/// combination of an icon and a label. This idiom appears across many kinds of
/// apps and shows up in collections, lists, menus of action items, and
/// disclosable lists, just to name a few.
///
/// You create a label, in its simplest form, by providing a title and the name
/// of an image, such as an icon from the
/// [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)
/// collection:
///
/// Label("Lightning", systemImage: "bolt.fill")
///
/// You can also apply styles to labels in several ways. In the case of dynamic
/// changes to the view after device rotation or change to a window size you
/// might want to show only the text portion of the label using the
/// ``LabelStyle/titleOnly`` label style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.titleOnly)
///
/// Conversely, there's also an icon-only label style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.iconOnly)
///
/// Some containers might apply a different default label style, such as only
/// showing icons within toolbars on macOS and iOS. To opt in to showing both
/// the title and the icon, you can apply the ``LabelStyle/titleAndIcon`` label
/// style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.titleAndIcon)
///
/// You can also create a customized label style by modifying an existing
/// style; this example adds a red border to the default label style:
///
/// struct RedBorderedLabelStyle: LabelStyle {
/// func makeBody(configuration: Configuration) -> some View {
/// Label(configuration)
/// .border(Color.red)
/// }
/// }
///
/// For more extensive customization or to create a completely new label style,
/// you'll need to adopt the ``LabelStyle`` protocol and implement a
/// ``LabelStyleConfiguration`` for the new style.
///
/// To apply a common label style to a group of labels, apply the style
/// to the view hierarchy that contains the labels:
///
/// VStack {
/// Label("Rain", systemImage: "cloud.rain")
/// Label("Snow", systemImage: "snow")
/// Label("Sun", systemImage: "sun.max")
/// }
/// .labelStyle(.iconOnly)
///
/// It's also possible to make labels using views to compose the label's icon
/// programmatically, rather than using a pre-made image. In this example, the
/// icon portion of the label uses a filled ``Circle`` overlaid
/// with the user's initials:
///
/// Label {
/// Text(person.fullName)
/// .font(.body)
/// .foregroundColor(.primary)
/// Text(person.title)
/// .font(.subheadline)
/// .foregroundColor(.secondary)
/// } icon: {
/// Circle()
/// .fill(person.profileColor)
/// .frame(width: 44, height: 44, alignment: .center)
/// .overlay(Text(person.initials))
/// }
///
public struct Label<Title, Icon>: View where Title: View, Icon: View {
@Environment(\.self) private var environment
@Environment(\.backportLabelStyle) private var style
private var config: Backport<Any>.LabelStyleConfiguration
/// Creates a label with a custom title and icon.
public init(@ViewBuilder title: () -> Title, @ViewBuilder icon: () -> Icon) {
config = .init(title: .init(content: title()), icon: .init(content: icon()))
}
@MainActor public var body: some View {
if let style = style {
style.makeBody(configuration: config.environment(environment))
} else {
DefaultLabelStyle()
.makeBody(configuration: config.environment(environment))
}
}
}
}
@available(macOS 10.15, *)
public extension Backport.Label where Wrapped == Any, Title == Text, Icon == Image {
/// Creates a label with an icon image and a title generated from a
/// localized string.
///
/// - Parameters:
/// - titleKey: A title generated from a localized string.
/// - image: The name of the image resource to lookup.
init(_ titleKey: LocalizedStringKey, image name: String) {
self.init(title: { Text(titleKey) }, icon: { Image(name) })
}
/// Creates a label with an icon image and a title generated from a string.
///
/// - Parameters:
/// - title: A string used as the label's title.
/// - image: The name of the image resource to lookup.
init<S>(_ title: S, image name: String) where S: StringProtocol {
self.init(title: { Text(title) }, icon: { Image(name) })
}
}
@available(macOS, introduced: 11, message: "SFSymbols support was only introduced in macOS 11")
extension Backport.Label where Wrapped == Any, Title == Text, Icon == Image {
/// Creates a label with a system icon image and a title generated from a
/// localized string.
///
/// - Parameters:
/// - titleKey: A title generated from a localized string.
/// - systemImage: The name of the image resource to lookup.
public init(_ titleKey: LocalizedStringKey, systemImage name: String) {
self.init(title: { Text(titleKey) }, icon: { Image(systemName: name) })
}
/// Creates a label with a system icon image and a title generated from a
/// string.
///
/// - Parameters:
/// - title: A string used as the label's title.
/// - systemImage: The name of the image resource to lookup.
public init<S>(_ title: S, systemImage name: String) where S: StringProtocol {
self.init(title: { Text(title) }, icon: { Image(systemName: name) })
}
}
@available(macOS 10.15, *)
public extension Backport.Label
where Wrapped == Any, Title == Backport.LabelStyleConfiguration.Title, Icon == Backport.LabelStyleConfiguration.Icon
{
/// Creates a label representing the configuration of a style.
///
/// You can use this initializer within the ``LabelStyle/makeBody(configuration:)``
/// method of a ``LabelStyle`` instance to create an instance of the label
/// that's being styled. This is useful for custom label styles that only
/// wish to modify the current style, as opposed to implementing a brand new
/// style.
///
/// For example, the following style adds a red border around the label,
/// but otherwise preserves the current style:
///
/// struct RedBorderedLabelStyle: LabelStyle {
/// func makeBody(configuration: Configuration) -> some View {
/// Label(configuration)
/// .border(Color.red)
/// }
/// }
///
/// - Parameter configuration: The label style to use.
init(_ configuration: Backport.LabelStyleConfiguration) {
config = configuration
}
}

View File

@ -1,52 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// The properties of a label.
public struct LabelStyleConfiguration {
/// A type-erased title view of a label.
public struct Title: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// A type-erased icon view of a label.
public struct Icon: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// A description of the labeled item.
public internal(set) var title: LabelStyleConfiguration.Title
@available(macOS 10.15, *)
/// A symbolic representation of the labeled item.
public internal(set) var icon: LabelStyleConfiguration.Icon
@available(macOS 10.15, *)
internal var environment: EnvironmentValues = .init()
@available(macOS 10.15, *)
func environment(_ values: EnvironmentValues) -> Self {
var config = self
config.environment = values
return config
}
}
}

View File

@ -1,72 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
/// A type that applies a custom appearance to all labels within a view.
///
/// To configure the current label style for a view hierarchy, use the
/// ``View/labelStyle(_:)`` modifier.
public protocol BackportLabelStyle {
/// The properties of a label.
typealias Configuration = Backport<Any>.LabelStyleConfiguration
/// A view that represents the body of a label.
associatedtype Body: View
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
@ViewBuilder func makeBody(configuration: Configuration) -> Body
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped: View {
public func labelStyle<S: BackportLabelStyle>(_ style: S) -> some View {
content.environment(\.backportLabelStyle, .init(style))
}
}
@available(macOS 10.15, *)
internal struct AnyLabelStyle: BackportLabelStyle {
let _makeBody: (Backport<Any>.LabelStyleConfiguration) -> AnyView
@available(macOS 10.15, *)
init<S: BackportLabelStyle>(_ style: S) {
_makeBody = { config in
AnyView(style.makeBody(configuration: config))
}
}
@available(macOS 10.15, *)
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
@available(macOS 10.15, *)
private struct BackportLabelStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: AnyLabelStyle?
}
@available(macOS 10.15, *)
extension EnvironmentValues {
var backportLabelStyle: AnyLabelStyle? {
get { self[BackportLabelStyleEnvironmentKey.self] }
set { self[BackportLabelStyleEnvironmentKey.self] = newValue }
}
}

View File

@ -1,44 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// The default label style in the current context.
///
/// You can also use ``LabelStyle/automatic`` to construct this style.
public struct DefaultLabelStyle: BackportLabelStyle {
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: DefaultLabelStyle.Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.DefaultLabelStyle {
/// A label style that resolves its appearance automatically based on the
/// current context.
public static var automatic: Self { .init() }
}

View File

@ -1,44 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// A label style that only displays the icon of the label.
///
/// You can also use ``LabelStyle/iconOnly`` to construct this style.
public struct IconOnlyLabelStyle: BackportLabelStyle {
/// Creates an icon-only label style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: Configuration) -> some View {
configuration.icon
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.IconOnlyLabelStyle {
/// A label style that only displays the icon of the label.
///
/// The title of the label is still used for non-visual descriptions, such as
/// VoiceOver.
public static var iconOnly: Self { .init() }
}

View File

@ -1,70 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// A label style that shows both the title and icon of the label using a
/// system-standard layout.
///
/// You can also use ``LabelStyle/titleAndIcon`` to construct this style.
public struct TitleAndIconLabelStyle: BackportLabelStyle {
/// Creates a label style that shows both the title and icon of the label
/// using a system-standard layout.
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.TitleAndIconLabelStyle {
/// A label style that shows both the title and icon of the label using a
/// system-standard layout.
///
/// In most cases, labels show both their title and icon by default. However,
/// some containers might apply a different default label style to their
/// content, such as only showing icons within toolbars on macOS and iOS. To
/// opt in to showing both the title and the icon, you can apply the title
/// and icon label style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.titleAndIcon)
///
/// To apply the title and icon style to a group of labels, apply the style
/// to the view hierarchy that contains the labels:
///
/// VStack {
/// Label("Rain", systemImage: "cloud.rain")
/// Label("Snow", systemImage: "snow")
/// Label("Sun", systemImage: "sun.max")
/// }
/// .labelStyle(.titleAndIcon)
///
/// The relative layout of the title and icon is dependent on the context it
/// is displayed in. In most cases, however, the label is arranged
/// horizontally with the icon leading.
public static var titleAndIcon: Self { .init() }
}

View File

@ -1,41 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
// A label style that only displays the title of the label.
///
/// You can also use ``LabelStyle/titleOnly`` to construct this style.
public struct TitleOnlyLabelStyle: BackportLabelStyle {
/// Creates a title-only label style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: Configuration) -> some View {
configuration.title
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.TitleOnlyLabelStyle {
/// A label style that only displays the title of the label.
public static var titleOnly: Self { .init() }
}

View File

@ -1,275 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped == Any {
/// A container for attaching a label to a value-bearing view.
///
/// The instance's content represents a read-only or read-write value, and its
/// label identifies or describes the purpose of that value.
/// The resulting element has a layout that's consistent with other framework
/// controls and automatically adapts to its container, like a form or toolbar.
/// Some styles of labeled content also apply styling or behaviors to the value
/// content, like making ``Text`` views selectable.
///
/// The following example associates a label with a custom view and has
/// a layout that matches the label of the ``Picker``:
///
/// Form {
/// Backport.LabeledContent("Custom Value") {
/// MyCustomView(value: $value)
/// }
/// Picker("Selected Value", selection: $selection) {
/// PickerOption("Option 1", 1)
/// PickerOption("Option 2", 2)
/// }
/// }
///
/// ### Custom view labels
///
/// You can assemble labeled content with an explicit view for its label
/// using the ``init(content:label:)`` initializer. For example, you can
/// rewrite the previous labeled content example using a ``Text`` view:
///
/// LabeledContent {
/// MyCustomView(value: $value)
/// } label: {
/// Text("Custom Value")
/// }
///
/// The `label` view builder accepts any kind of view, like a ``Label``:
///
/// Backport.LabeledContent {
/// MyCustomView(value: $value)
/// } label: {
/// Label("Custom Value", systemImage: "hammer")
/// }
///
/// ### Textual labeled content
///
/// You can construct labeled content with string values or formatted values
/// to create read-only displays of textual values:
///
/// Form {
/// Section("Information") {
/// Backport.LabeledContent("Name", value: person.name)
/// }
/// if !person.pets.isEmpty {
/// Section("Pets") {
/// ForEach(pet) { pet in
/// Backport.LabeledContent(pet.species, value: pet.name)
/// }
/// }
/// }
/// }
///
/// Wherever possible, SwiftUI makes this text selectable.
///
/// ### Compositional elements
///
/// You can use labeled content as the label for other elements. For example,
/// a ``NavigationLink`` can present a summary value for the destination it
/// links to:
///
/// Form {
/// NavigationLink(value: Settings.wifiDetail) {
/// Backport.LabeledContent("Wi-Fi", value: ssidName)
/// }
/// }
///
/// In some cases, the styling of views used as the value content is
/// specialized as well. For example, while a ``Toggle`` in an inset group
/// form on macOS is styled as a switch by default, it's styled as a checkbox
/// when used as a value element within a surrounding `LabeledContent`
/// instance:
///
/// Form {
/// Backport.LabeledContent("Source Control") {
/// Toggle("Refresh local status automatically",
/// isOn: $refreshLocalStatus)
/// Toggle("Fetch and refresh server status automatically",
/// isOn: $refreshServerStatus)
/// Toggle("Add and remove files automatically",
/// isOn: $addAndRemoveFiles)
/// Toggle("Select files to commit automatically",
/// isOn: $selectFiles)
/// }
/// }
///
/// ### Controlling label visibility
///
/// A label communicates the identity or purpose of the value, which is
/// important for accessibility. However, you might want to hide the label
/// in the display, and some controls or contexts may visually hide their label
/// by default. The ``View/labels(_:)`` modifier allows controlling that
/// visibility. The following example hides both labels, producing only a
/// group of the two value views:
///
/// Group {
/// LabeledContent("Custom Value") {
/// MyCustomView(value: $value)
/// }
/// Picker("Selected Value", selection: $selection) {
/// PickerOption("Option 1", 1)
/// PickerOption("Option 2", 2)
/// }
/// }
/// .labelsHidden()
///
/// ### Styling labeled content
///
/// You can set label styles using the ``View/labeledContentStyle(_:)``
/// modifier. You can also build custom styles using ``LabeledContentStyle``.
public struct LabeledContent<Label, Content> {
@Environment(\.backportLabeledContentStyle) private var style
let config: LabeledContentStyleConfiguration
public var body: some View {
style.makeBody(configuration: config)
}
}
}
@available(macOS 10.15, *)
public extension Backport.LabeledContent
where
Wrapped == Any, Label == Backport<Any>.LabeledContentStyleConfiguration.Label,
Content == Backport<Any>.LabeledContentStyleConfiguration.Content
{
/// Creates labeled content based on a labeled content style configuration.
///
/// You can use this initializer within the
/// ``LabeledContentStyle/makeBody(configuration:)`` method of a
/// ``LabeledContentStyle`` to create a labeled content instance.
/// This is useful for custom styles that only modify the current style,
/// as opposed to implementing a brand new style.
///
/// For example, the following style adds a red border around the labeled
/// content, but otherwise preserves the current style:
///
/// struct RedBorderLabeledContentStyle: LabeledContentStyle {
/// func makeBody(configuration: Configuration) -> some View {
/// LabeledContent(configuration)
/// .border(.red)
/// }
/// }
///
/// - Parameter configuration: The properties of the labeled content
init(_ config: Backport.LabeledContentStyleConfiguration) {
self.config = config
}
}
@available(macOS 10.15, *)
public extension Backport.LabeledContent where Wrapped == Any, Label == Text, Content: View {
/// Creates a labeled view that generates its label from a localized string
/// key.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// - Parameters:
/// - titleKey: The key for the view's localized title, that describes
/// the purpose of the view.
/// - content: The value content being labeled.
init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) {
config = .init(
label: Text(titleKey),
content: content()
)
}
@available(macOS 10.15, *)
/// Creates a labeled view that generates its label from a string.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
/// information about localizing strings.
///
/// - Parameters:
/// - title: A string that describes the purpose of the view.
/// - content: The value content being labeled.
init<S>(_ title: S, @ViewBuilder content: () -> Content) where S: StringProtocol {
config = .init(
label: Text(title),
content: content()
)
}
}
@available(macOS 10.15, *)
extension Backport.LabeledContent: View where Wrapped == Any, Label: View, Content: View {
/// Creates a labeled view that generates its label from a localized string
/// key.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// - Parameters:
/// - titleKey: The key for the view's localized title, that describes
/// the purpose of the view.
/// - content: The value content being labeled.
public init(@ViewBuilder content: () -> Content, @ViewBuilder label: () -> Label) {
config = .init(
label: label(),
content: content()
)
}
}
@available(macOS 10.15, *)
public extension Backport.LabeledContent where Wrapped == Any, Label == Text, Content == Text {
/// Creates a labeled informational view.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// Form {
/// LabeledContent("Name", value: person.name)
/// }
///
/// In some contexts, this text will be selectable by default.
///
/// - Parameters:
/// - titleKey: The key for the view's localized title, that describes
/// the purpose of the view.
/// - value: The value being labeled.
init<S: StringProtocol>(_ titleKey: LocalizedStringKey, value: S) {
config = .init(
label: Text(titleKey),
content: Text(value)
)
}
@available(macOS 10.15, *)
/// Creates a labeled informational view.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
/// information about localizing strings.
///
/// Form {
/// ForEach(person.pet) { pet in
/// LabeledContent(pet.species, value: pet.name)
/// }
/// }
///
/// - Parameters:
/// - title: A string that describes the purpose of the view.
/// - value: The value being labeled.
init<S1: StringProtocol, S2: StringProtocol>(_ title: S1, value: S2) {
config = .init(
label: Text(title),
content: Text(value)
)
}
}

View File

@ -1,63 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped: View {
/// Sets a style for labeled content.
public func labeledContentStyle<S>(_ style: S) -> some View where S: BackportLabeledContentStyle {
content.environment(\.backportLabeledContentStyle, .init(style))
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
public protocol BackportLabeledContentStyle {
typealias Configuration = Backport<Any>.LabeledContentStyleConfiguration
associatedtype Body: View
@ViewBuilder func makeBody(configuration: Configuration) -> Body
}
@available(macOS 10.15, *)
internal struct AnyLabeledContentStyle: BackportLabeledContentStyle {
typealias Configuration = Backport<Any>.LabeledContentStyleConfiguration
let _makeBody: (Configuration) -> AnyView
@available(macOS 10.15, *)
init<S: BackportLabeledContentStyle>(_ style: S) {
_makeBody = { config in
AnyView(style.makeBody(configuration: config))
}
}
@available(macOS 10.15, *)
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
@available(macOS 10.15, *)
private struct BackportLabeledContentStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: AnyLabeledContentStyle = .init(.automatic)
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension EnvironmentValues {
var backportLabeledContentStyle: AnyLabeledContentStyle {
get { self[BackportLabeledContentStyleEnvironmentKey.self] }
set { self[BackportLabeledContentStyleEnvironmentKey.self] = newValue }
}
}

View File

@ -1,70 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped == Any {
/// The properties of a labeled content instance.
public struct LabeledContentStyleConfiguration {
/// A type-erased label of a labeled content instance.
public struct Label: View {
@EnvironmentContains(key: "LabelsHiddenKey") private var isHidden
let view: AnyView
public var body: some View {
if isHidden {
EmptyView()
} else {
view
}
}
@available(macOS 10.15, *)
init<V: View>(_ view: V) {
self.view = .init(view)
}
}
@available(macOS 10.15, *)
/// A type-erased content of a labeled content instance.
public struct Content: View {
@EnvironmentContains(key: "LabelsHiddenKey") private var isHidden
let view: AnyView
public var body: some View {
view
.foregroundColor(isHidden ? .primary : .secondary)
.frame(maxWidth: .infinity, alignment: isHidden ? .leading : .trailing)
}
@available(macOS 10.15, *)
init<V: View>(_ view: V) {
self.view = .init(view)
}
}
@available(macOS 10.15, *)
/// The label of the labeled content instance.
public let label: Label
@available(macOS 10.15, *)
/// The content of the labeled content instance.
public let content: Content
@available(macOS 10.15, *)
internal init<L: View, C: View>(label: L, content: C) {
self.label = .init(label)
self.content = .init(content)
}
@available(macOS 10.15, *)
internal init<L: View, C: View>(@ViewBuilder content: () -> C, @ViewBuilder label: () -> L) {
self.content = .init(content())
self.label = .init(label())
}
}
}

View File

@ -1,24 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped == Any {
struct AutomaticLabeledContentStyle: BackportLabeledContentStyle {
public func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .firstTextBaseline) {
configuration.label
Spacer()
configuration.content
.multilineTextAlignment(.trailing)
}
}
}
}
@available(macOS 10.15, *)
extension BackportLabeledContentStyle where Self == Backport<Any>.AutomaticLabeledContentStyle {
static var automatic: Self { .init() }
}

View File

@ -1,144 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(watchOS, deprecated: 9)
@available(macOS, deprecated: 13)
extension Backport where Wrapped: View {
/// Associates a destination view with a presented data type for use within
/// a navigation stack.
///
/// Add this view modifer to a view inside a ``NavigationStack`` to
/// describe the view that the stack displays when presenting
/// a particular kind of data. Use a ``NavigationLink`` to present
/// the data. For example, you can present a `ColorDetail` view for
/// each presentation of a ``Color`` instance:
///
/// NavigationStack {
/// List {
/// NavigationLink("Mint", value: Color.mint)
/// NavigationLink("Pink", value: Color.pink)
/// NavigationLink("Teal", value: Color.teal)
/// }
/// .navigationDestination(for: Color.self) { color in
/// ColorDetail(color: color)
/// }
/// .navigationTitle("Colors")
/// }
///
/// You can add more than one navigation destination modifier to the stack
/// if it needs to present more than one kind of data.
///
/// - Parameters:
/// - data: The type of data that this destination matches.
/// - destination: A view builder that defines a view to display
/// when the stack's navigation state contains a value of
/// type `data`. The closure takes one argument, which is the value
/// of the data to present.
public func navigationDestination<D: Hashable, C: View>(for _: D.Type, @ViewBuilder destination: @escaping (D) -> C)
-> some View
{
content
.environment(
\.navigationDestinations,
[
.init(type: D.self): .init { destination($0 as! D) },
]
)
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(watchOS, deprecated: 9)
@available(macOS, deprecated: 13)
extension Backport where Wrapped == Any {
public struct NavigationLink<Label, Destination>: View where Label: View, Destination: View {
@Environment(\.navigationDestinations) private var destinations
@available(macOS 10.15, *)
private let valueType: AnyMetaType
private let value: Any?
private let label: Label
private let destination: () -> Destination
@available(macOS 10.15, *)
public init<P>(value: P?, @ViewBuilder label: () -> Label) where Destination == Never {
self.value = value
valueType = .init(type: P.self)
destination = { fatalError() }
self.label = label()
}
@available(macOS 10.15, *)
public var body: some View {
SwiftUI.NavigationLink {
if let value = value {
destinations[valueType.type]?.content(value)
}
} label: {
label
}
.disabled(value == nil)
}
}
}
@available(macOS 10.15, *)
private struct NavigationDestinationsEnvironmentKey: EnvironmentKey {
static var defaultValue: [AnyMetaType: DestinationView] = [:]
}
@available(macOS 10.15, *)
private extension EnvironmentValues {
var navigationDestinations: [AnyMetaType: DestinationView] {
get { self[NavigationDestinationsEnvironmentKey.self] }
set {
var current = self[NavigationDestinationsEnvironmentKey.self]
newValue.forEach { current[$0] = $1 }
self[NavigationDestinationsEnvironmentKey.self] = current
}
}
}
@available(macOS 10.15, *)
private struct AnyMetaType {
let type: Any.Type
}
@available(macOS 10.15, *)
extension AnyMetaType: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.type == rhs.type
}
}
@available(macOS 10.15, *)
extension AnyMetaType: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(type))
}
}
@available(macOS 10.15, *)
private extension Dictionary {
subscript(_ key: Any.Type) -> Value? where Key == AnyMetaType {
get { self[.init(type: key)] }
_modify { yield &self[.init(type: key)] }
}
}
@available(macOS 10.15, *)
private struct DestinationView: View {
let content: (Any) -> AnyView
var body: Never { fatalError() }
init<Content: View>(content: @escaping (Any) -> Content) {
self.content = { AnyView(content($0)) }
}
}

View File

@ -1,38 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(tvOS, deprecated: 14)
extension Backport where Wrapped: View {
@ViewBuilder
public func navigationTitle<S: StringProtocol>(_ title: S) -> some View {
#if os(macOS)
if #available(macOS 11, *) {
content.navigationTitle(title)
} else {
content
}
#else
content.navigationBarTitle(title)
#endif
}
@available(macOS 10.15, *)
@ViewBuilder
public func navigationTitle(_ titleKey: LocalizedStringKey) -> some View {
#if os(macOS)
if #available(macOS 11, *) {
content.navigationTitle(titleKey)
} else {
content
}
#else
content.navigationBarTitle(titleKey)
#endif
}
}

View File

@ -1,55 +0,0 @@
import Combine
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14.0)
@available(macOS, deprecated: 11.0)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped: View {
/// Adds a modifier for this view that fires an action when a specific
/// value changes.
///
/// `onChange` is called on the main thread. Avoid performing long-running
/// tasks on the main thread. If you need to perform a long-running task in
/// response to `value` changing, you should dispatch to a background queue.
///
/// The new value is passed into the closure.
///
/// - Parameters:
/// - value: The value to observe for changes
/// - action: A closure to run when the value changes.
/// - newValue: The new value that changed
///
/// - Returns: A view that fires an action when the specified value changes.
@ViewBuilder
public func onChange<Value: Equatable>(of value: Value, perform action: @escaping (Value) -> Void) -> some View {
content.modifier(ChangeModifier(value: value, action: action))
}
}
@available(macOS 10.15, *)
private struct ChangeModifier<Value: Equatable>: ViewModifier {
let value: Value
let action: (Value) -> Void
@available(macOS 10.15, *)
@State var oldValue: Value?
@available(macOS 10.15, *)
init(value: Value, action: @escaping (Value) -> Void) {
self.value = value
self.action = action
_oldValue = .init(initialValue: value)
}
@available(macOS 10.15, *)
func body(content: Content) -> some View {
content
.onReceive(Just(value)) { newValue in
guard newValue != oldValue else { return }
action(newValue)
oldValue = newValue
}
}
}

View File

@ -1,177 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if canImport(WatchKit)
import WatchKit
#endif
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(tvOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// An action that opens a URL.
///
/// Read the ``EnvironmentValues.backportOpenURL`` environment value to get an
/// instance of this structure for a given ``Environment``. Call the
/// instance to open a URL. You call the instance directly because it
/// defines a ``Backport.OpenURLAction.callAsFunction(_:)`` method that Swift
/// calls when you call the instance.
///
/// For example, you can open a web site when the user taps a button:
///
/// struct OpenURLExample: View {
/// @Environment(\.backportOpenURL) private var openURL
///
/// var body: some View {
/// Button {
/// if let url = URL(string: "https://www.example.com") {
/// openURL(url)
/// }
/// } label: {
/// Label("Get Help", systemImage: "person.fill.questionmark")
/// }
/// }
/// }
///
/// If you want to know whether the action succeeds, add a completion
/// handler that takes a Boolean value. In this case, Swift implicitly
/// calls the ``Backport.OpenURLAction.callAsFunction(_:completion:)`` method
/// instead. That method calls your completion handler after it determines
/// whether it can open the URL, but possibly before it finishes opening
/// the URL. You can add a handler to the example above so that
/// it prints the outcome to the console:
///
/// openURL(url) { accepted in
/// print(accepted ? "Success" : "Failure")
/// }
///
/// The system provides a default open URL action with behavior
/// that depends on the contents of the URL. For example, the default
/// action opens a Universal Link in the associated app if possible,
/// or in the users default web browser if not.
///
/// You can also set a custom action using the ``View.environment(_:_:)``
/// view modifier. Any views that read the action from the environment,
/// including the built-in ``Link`` view and ``Text`` views with markdown
/// links, or links in attributed strings, use your action. Initialize an
/// action by calling the ``Backport.OpenURLAction.init(handler:)`` initializer with
/// a handler that takes a URL and returns an ``Backport.OpenURLAction.Result``:
///
/// Text("Visit [Example Company](https://www.example.com) for details.")
/// .environment(\.backportOpenURL, Backport.OpenURLAction { url in
/// handleURL(url) // Define this method to take appropriate action.
/// return .handled
/// })
///
/// SwiftUI translates the value that your custom action's handler
/// returns into an appropriate Boolean result for the action call.
/// For example, a view that uses the action declared above
/// receives `true` when calling the action, because the
/// handler always returns ``Backport.OpenURLAction.Result.handled``.
public struct OpenURLAction {
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
public struct Result {
enum Value {
case handled
case discarded
case systemAction(_ url: URL?)
@available(macOS 10.15, *)
var accepted: Bool {
if case .discarded = self {
return false
} else {
return true
}
}
}
@available(macOS 10.15, *)
let value: Value
@available(macOS 10.15, *)
public static var handled: Result { .init(value: .handled) }
public static var discarded: Result { .init(value: .discarded) }
public static var systemAction: Result { .init(value: .systemAction(nil)) }
public static func systemAction(_ url: URL) -> Result { .init(value: .systemAction(url)) }
}
@available(macOS 10.15, *)
let handler: (URL) -> Result
@available(macOS 10.15, *)
public init(handler: @escaping (URL) -> Result) {
self.handler = handler
}
@available(macOS 10.15, *)
@available(watchOS, unavailable)
public func callAsFunction(_ url: URL) {
handleUrl(url)
}
@available(macOS 10.15, *)
@available(watchOS, unavailable)
public func callAsFunction(_ url: URL, completion: @escaping (_ accepted: Bool) -> Void) {
let result = handleUrl(url)
completion(result.accepted)
}
@available(macOS 10.15, *)
@discardableResult
private func handleUrl(_ url: URL) -> Result.Value {
let result = handler(url).value
switch result {
case .handled, .discarded: break
case let .systemAction(updatedUrl):
let resolved = updatedUrl ?? url
#if os(macOS)
NSWorkspace.shared.open(resolved)
#elseif os(iOS) || os(tvOS)
UIApplication.shared.open(resolved)
#else
WKExtension.shared().openSystemURL(resolved)
#endif
}
return result
}
}
}
@available(macOS 10.15, *)
private struct BackportOpenURLKey: EnvironmentKey {
static var defaultValue: Backport<Any>.OpenURLAction {
.init { url in
#if os(macOS)
return .systemAction
#elseif os(iOS) || os(tvOS)
if UIApplication.shared.canOpenURL(url) {
return .systemAction
} else {
return .discarded
}
#else
return .systemAction
#endif
}
}
}
@available(macOS 10.15, *)
public extension EnvironmentValues {
var backportOpenURL: Backport<Any>.OpenURLAction {
get { self[BackportOpenURLKey.self] }
set { self[BackportOpenURLKey.self] = newValue }
}
}

View File

@ -1,127 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Layers the views that you specify in front of this view.
///
/// Use this modifier to place one or more views in front of another view.
/// For example, you can place a group of stars on a ``RoundedRectangle``:
///
/// RoundedRectangle(cornerRadius: 8)
/// .frame(width: 200, height: 100)
/// .overlay(alignment: .topLeading) { Star(color: .red) }
/// .overlay(alignment: .topTrailing) { Star(color: .yellow) }
/// .overlay(alignment: .bottomLeading) { Star(color: .green) }
/// .overlay(alignment: .bottomTrailing) { Star(color: .blue) }
///
/// The example above assumes that you've defined a `Star` view with a
/// parameterized color:
///
/// struct Star: View {
/// var color = Color.yellow
///
/// var body: some View {
/// Image(systemName: "star.fill")
/// .foregroundStyle(color)
/// }
/// }
///
/// By setting different `alignment` values for each modifier, you make the
/// stars appear in different places on the rectangle:
///
/// ![A screenshot of a rounded rectangle with a star in each corner. The
/// star in the upper-left is red; the start in the upper-right is yellow;
/// the star in the lower-left is green; the star the lower-right is
/// blue.](View-overlay-2)
///
/// If you specify more than one view in the `content` closure, the modifier
/// collects all of the views in the closure into an implicit ``ZStack``,
/// taking them in order from back to front. For example, you can place a
/// star and a ``Circle`` on a field of ``ShapeStyle/blue``:
///
/// Color.blue
/// .frame(width: 200, height: 200)
/// .overlay {
/// Circle()
/// .frame(width: 100, height: 100)
/// Star()
/// }
///
/// Both the overlay modifier and the implicit ``ZStack`` composed from the
/// overlay content --- the circle and the star --- use a default
/// ``Alignment/center`` alignment. The star appears centered on the circle,
/// and both appear as a composite view centered in front of the square:
///
/// ![A screenshot of a star centered on a circle, which is
/// centered on a square.](View-overlay-3)
///
/// If you specify an alignment for the overlay, it applies to the implicit
/// stack rather than to the individual views in the closure. You can see
/// this if you add the ``Alignment/bottom`` alignment:
///
/// Color.blue
/// .frame(width: 200, height: 200)
/// .overlay(alignment: .bottom) {
/// Circle()
/// .frame(width: 100, height: 100)
/// Star()
/// }
///
/// The circle and the star move down as a unit to align the stack's bottom
/// edge with the bottom edge of the square, while the star remains
/// centered on the circle:
///
/// ![A screenshot of a star centered on a circle, which is on a square.
/// The circle's bottom edge is aligned with the square's bottom
/// edge.](View-overlay-3a)
///
/// To control the placement of individual items inside the `content`
/// closure, either use a different overlay modifier for each item, as the
/// earlier example of stars in the corners of a rectangle demonstrates, or
/// add an explicit ``ZStack`` inside the content closure with its own
/// alignment:
///
/// Color.blue
/// .frame(width: 200, height: 200)
/// .overlay(alignment: .bottom) {
/// ZStack(alignment: .bottom) {
/// Circle()
/// .frame(width: 100, height: 100)
/// Star()
/// }
/// }
///
/// The stack alignment ensures that the star's bottom edge aligns with the
/// circle's, while the overlay aligns the composite view with the square:
///
/// ![A screenshot of a star, a circle, and a square with all their
/// bottom edges aligned.](View-overlay-4)
///
/// You can achieve layering without an overlay modifier by putting both the
/// modified view and the overlay content into a ``ZStack``. This can
/// produce a simpler view hierarchy, but changes the layout priority that
/// SwiftUI applies to the views. Use the overlay modifier when you want the
/// modified view to dominate the layout.
///
/// If you want to specify a ``ShapeStyle`` like a ``Color`` or a
/// ``Material`` as the overlay, use
/// ``View/overlay(_:ignoresSafeAreaEdges:)`` instead. To specify a
/// ``Shape``, use ``View/overlay(_:in:fillStyle:)``.
///
/// - Parameters:
/// - alignment: The alignment that the modifier uses to position the
/// implicit ``ZStack`` that groups the foreground views. The default
/// is ``Alignment/center``.
/// - content: A ``ViewBuilder`` that you use to declare the views to
/// draw in front of this view, stacked in the order that you list them.
/// The last view that you list appears at the front of the stack.
///
/// - Returns: A view that uses the specified content as a foreground.
func overlay<Content: View>(alignment: Alignment = .center, @ViewBuilder _ content: () -> Content) -> some View {
self.content.overlay(content(), alignment: alignment)
}
}

View File

@ -1,256 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
public struct ProgressView<Label: View, CurrentValueLabel: View>: View {
@Environment(\.backportProgressViewStyle) private var style
let config: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
public var body: some View {
Group {
if let style = style {
style.makeBody(configuration: config)
} else {
DefaultProgressViewStyle().makeBody(configuration: config)
}
}
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, unavailable)
extension Backport.ProgressView where Wrapped == Any, CurrentValueLabel == EmptyView {
/// Creates a progress view for showing indeterminate progress, without a
/// label.
public init() where Label == EmptyView {
self.init(config: .init(fractionCompleted: nil, preferredKind: .circular))
}
@available(macOS 10.15, *)
/// Creates a progress view for showing indeterminate progress that displays
/// a custom label.
///
/// - Parameters:
/// - label: A view builder that creates a view that describes the task
/// in progress.
public init(@ViewBuilder label: () -> Label) {
config = .init(fractionCompleted: nil, label: .init(content: label()), preferredKind: .circular)
}
@available(macOS 10.15, *)
/// Creates a progress view for showing indeterminate progress that
/// generates its label from a localized string.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// ``Text`` for more information about localizing strings. To initialize a
/// indeterminate progress view with a string variable, use
/// the corresponding initializer that takes a `StringProtocol` instance.
///
/// - Parameters:
/// - titleKey: The key for the progress view's localized title that
/// describes the task in progress.
public init(_ titleKey: LocalizedStringKey) where Label == Text {
config = .init(fractionCompleted: nil, label: .init(content: Text(titleKey)), preferredKind: .circular)
}
@available(macOS 10.15, *)
/// Creates a progress view for showing indeterminate progress that
/// generates its label from a string.
///
/// - Parameters:
/// - title: A string that describes the task in progress.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// title similar to ``Text/init(verbatim:)``. See ``Text`` for more
/// information about localizing strings. To initialize a progress view with
/// a localized string key, use the corresponding initializer that takes a
/// `LocalizedStringKey` instance.
public init<S>(_ title: S) where Label == Text, S: StringProtocol {
config = .init(fractionCompleted: nil, label: .init(content: Text(title)), preferredKind: .circular)
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport.ProgressView where Wrapped == Any {
/// Creates a progress view for showing determinate progress.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// - Parameters:
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
public init<V>(value: V?, total: V = 1.0)
where Label == EmptyView, CurrentValueLabel == EmptyView, V: BinaryFloatingPoint
{
if let value = value {
config = .init(fractionCompleted: Double(value) / Double(total), preferredKind: .linear, max: Double(total))
} else {
config = .init(fractionCompleted: nil, preferredKind: .linear)
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress, with a
/// custom label.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// - Parameters:
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
/// - label: A view builder that creates a view that describes the task
/// in progress.
public init<V>(value: V?, total: V = 1.0, @ViewBuilder label: () -> Label)
where CurrentValueLabel == EmptyView, V: BinaryFloatingPoint
{
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: label()), preferredKind: .linear
)
} else {
config = .init(fractionCompleted: nil, label: .init(content: label()), preferredKind: .linear, max: Double(total))
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress, with a
/// custom label.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// - Parameters:
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
/// - label: A view builder that creates a view that describes the task
/// in progress.
/// - currentValueLabel: A view builder that creates a view that
/// describes the level of completed progress of the task.
public init<V>(
value: V?, total: V = 1.0, @ViewBuilder label: () -> Label, @ViewBuilder currentValueLabel: () -> CurrentValueLabel
) where V: BinaryFloatingPoint {
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: label()),
currentValueLabel: .init(content: currentValueLabel()), preferredKind: .linear, max: Double(total)
)
} else {
config = .init(
fractionCompleted: nil, label: .init(content: label()), currentValueLabel: .init(content: currentValueLabel()),
preferredKind: .linear, max: Double(total)
)
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress that generates
/// its label from a localized string.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// ``Text`` for more information about localizing strings. To initialize a
/// determinate progress view with a string variable, use
/// the corresponding initializer that takes a `StringProtocol` instance.
///
/// - Parameters:
/// - titleKey: The key for the progress view's localized title that
/// describes the task in progress.
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is
/// indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
public init<V>(_ titleKey: LocalizedStringKey, value: V?, total: V = 1.0)
where Label == Text, CurrentValueLabel == EmptyView, V: BinaryFloatingPoint
{
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: Text(titleKey)), preferredKind: .linear,
max: Double(total)
)
} else {
config = .init(
fractionCompleted: nil, label: .init(content: Text(titleKey)), preferredKind: .linear, max: Double(total)
)
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress that generates
/// its label from a string.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// title similar to ``Text/init(verbatim:)``. See ``Text`` for more
/// information about localizing strings. To initialize a determinate
/// progress view with a localized string key, use the corresponding
/// initializer that takes a `LocalizedStringKey` instance.
///
/// - Parameters:
/// - title: The string that describes the task in progress.
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is
/// indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
public init<S, V>(_ title: S, value: V?, total: V = 1.0)
where Label == Text, CurrentValueLabel == EmptyView, S: StringProtocol, V: BinaryFloatingPoint
{
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: Text(title)), preferredKind: .linear,
max: Double(total)
)
} else {
config = .init(
fractionCompleted: nil, label: .init(content: Text(title)), preferredKind: .linear, max: Double(total)
)
}
}
}

View File

@ -1,73 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// The properties of a progress view instance.
public struct ProgressViewStyleConfiguration {
internal enum Kind {
case circular
case linear
}
@available(macOS 10.15, *)
/// A type-erased label describing the task represented by the progress
/// view.
public struct Label: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// A type-erased label that describes the current value of a progress view.
public struct CurrentValueLabel: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// The completed fraction of the task represented by the progress view,
/// from `0.0` (not yet started) to `1.0` (fully complete), or `nil` if the
/// progress is indeterminate or relative to a date interval.
public let fractionCompleted: Double?
@available(macOS 10.15, *)
/// A view that describes the task represented by the progress view.
///
/// If `nil`, then the task is self-evident from the surrounding context,
/// and the style does not need to provide any additional description.
///
/// If the progress view is defined using a `Progress` instance, then this
/// label is equivalent to its `localizedDescription`.
public var label: Label?
@available(macOS 10.15, *)
/// A view that describes the current value of a progress view.
///
/// If `nil`, then the value of the progress view is either self-evident
/// from the surrounding context or unknown, and the style does not need to
/// provide any additional description.
///
/// If the progress view is defined using a `Progress` instance, then this
/// label is equivalent to its `localizedAdditionalDescription`.
public var currentValueLabel: CurrentValueLabel?
@available(macOS 10.15, *)
internal let preferredKind: Kind
internal var min: Double = 0
internal var max: Double = 1
}
}

View File

@ -1,74 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
@available(macOS 10.15, *)
/// A type that applies standard interaction behavior to all progress views
/// within a view hierarchy.
///
/// To configure the current progress view style for a view hierarchy, use the
/// ``View/progressViewStyle(_:)`` modifier.
public protocol BackportProgressViewStyle {
/// A type alias for the properties of a progress view instance.
typealias Configuration = Backport<Any>.ProgressViewStyleConfiguration
/// A view representing the body of a progress view.
associatedtype Body: View
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
@ViewBuilder func makeBody(configuration: Configuration) -> Body
}
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
@available(macOS 10.15, *)
extension Backport where Wrapped: View {
public func progressViewStyle<S: BackportProgressViewStyle>(_ style: S) -> some View {
content.environment(\.backportProgressViewStyle, .init(style))
}
}
@available(macOS 10.15, *)
internal struct AnyProgressViewStyle: BackportProgressViewStyle {
let _makeBody: (Backport<Any>.ProgressViewStyleConfiguration) -> AnyView
init<S: BackportProgressViewStyle>(_ style: S) {
_makeBody = { config in
AnyView(style.makeBody(configuration: config))
}
}
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
@available(macOS 10.15, *)
private struct BackportProgressViewStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: AnyProgressViewStyle?
}
@available(macOS 10.15, *)
extension EnvironmentValues {
var backportProgressViewStyle: AnyProgressViewStyle? {
get { self[BackportProgressViewStyleEnvironmentKey.self] }
set { self[BackportProgressViewStyleEnvironmentKey.self] = newValue }
}
}

View File

@ -1,89 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// A progress view that visually indicates its progress using a circular gauge.
///
/// You can also use ``ProgressViewStyle/circular`` to construct this style.
public struct CircularProgressViewStyle: BackportProgressViewStyle {
/// Creates a circular progress view style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
public func makeBody(configuration: Configuration) -> some View {
VStack {
#if !os(watchOS)
CircularRepresentable(configuration: configuration)
#endif
configuration.label?
.foregroundColor(.secondary)
}
}
}
}
@available(macOS 10.15, *)
public extension BackportProgressViewStyle where Self == Backport<Any>.CircularProgressViewStyle {
static var circular: Self { .init() }
}
#if os(macOS)
@available(macOS 10.15, *)
private struct CircularRepresentable: NSViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeNSView(context _: Context) -> NSProgressIndicator {
.init()
}
@available(macOS 10.15, *)
func updateNSView(_ view: NSProgressIndicator, context _: Context) {
if let value = configuration.fractionCompleted {
view.doubleValue = value
view.maxValue = configuration.max
}
view.isIndeterminate = configuration.fractionCompleted == nil
view.style = .spinning
view.isDisplayedWhenStopped = true
view.startAnimation(nil)
}
}
#elseif !os(watchOS)
@available(macOS 10.15, *)
private struct CircularRepresentable: UIViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeUIView(context _: Context) -> UIActivityIndicatorView {
.init(style: .medium)
}
@available(macOS 10.15, *)
func updateUIView(_ view: UIActivityIndicatorView, context _: Context) {
view.hidesWhenStopped = false
view.startAnimating()
}
}
#endif

View File

@ -1,54 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// The default progress view style in the current context of the view being
/// styled.
///
/// You can also use ``ProgressViewStyle/automatic`` to construct this style.
public struct DefaultProgressViewStyle: BackportProgressViewStyle {
/// Creates a default progress view style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
public func makeBody(configuration: Configuration) -> some View {
switch configuration.preferredKind {
case .circular:
Backport.CircularProgressViewStyle().makeBody(configuration: configuration)
case .linear:
#if os(iOS)
if configuration.fractionCompleted == nil {
Backport.CircularProgressViewStyle().makeBody(configuration: configuration)
} else {
Backport.LinearProgressViewStyle().makeBody(configuration: configuration)
}
#else
Backport.LinearProgressViewStyle().makeBody(configuration: configuration)
#endif
}
}
}
}
@available(macOS 10.15, *)
public extension BackportProgressViewStyle where Self == Backport<Any>.DefaultProgressViewStyle {
static var automatic: Self { .init() }
}

View File

@ -1,111 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// A progress view that visually indicates its progress using a horizontal bar.
///
/// You can also use ``ProgressViewStyle/linear`` to construct this style.
public struct LinearProgressViewStyle: BackportProgressViewStyle {
/// Creates a linear progress view style.
public init() {}
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
@available(macOS 10.15, *)
public func makeBody(configuration: Configuration) -> some View {
#if os(macOS)
VStack(alignment: .leading, spacing: 0) {
configuration.label
.foregroundColor(.primary)
LinearRepresentable(configuration: configuration)
configuration.currentValueLabel
.foregroundColor(.secondary)
}
.controlSize(.small)
#else
VStack(alignment: .leading, spacing: 5) {
if configuration.fractionCompleted == nil {
CircularProgressViewStyle().makeBody(configuration: configuration)
} else {
configuration.label?
.foregroundColor(.primary)
#if !os(watchOS)
LinearRepresentable(configuration: configuration)
#endif
configuration.currentValueLabel?
.foregroundColor(.secondary)
.font(.caption)
}
}
#endif
}
}
}
@available(macOS 10.15, *)
public extension BackportProgressViewStyle where Self == Backport<Any>.LinearProgressViewStyle {
static var linear: Self { .init() }
}
#if os(macOS)
@available(macOS 10.15, *)
private struct LinearRepresentable: NSViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeNSView(context _: Context) -> NSProgressIndicator {
.init()
}
@available(macOS 10.15, *)
func updateNSView(_ view: NSProgressIndicator, context _: Context) {
if let value = configuration.fractionCompleted {
view.doubleValue = value
view.maxValue = configuration.max
view.display()
}
view.style = .bar
view.isIndeterminate = configuration.fractionCompleted == nil
view.isDisplayedWhenStopped = true
view.startAnimation(nil)
}
}
#elseif !os(watchOS)
@available(macOS 10.15, *)
private struct LinearRepresentable: UIViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeUIView(context _: Context) -> UIProgressView {
.init(progressViewStyle: .default)
}
@available(macOS 10.15, *)
func updateUIView(_ view: UIProgressView, context _: Context) {
view.progress = Float(configuration.fractionCompleted ?? 0)
}
}
#endif

View File

@ -1,88 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(iOS)
import QuickLook
final class PreviewController<Items>: UIViewController, UIAdaptivePresentationControllerDelegate,
QLPreviewControllerDelegate, QLPreviewControllerDataSource
where Items: RandomAccessCollection, Items.Element == URL
{
var items: Items
var selection: Binding<Items.Element?> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: selection.wrappedValue
)
}
}
init(selection: Binding<Items.Element?>, in items: Items) {
self.selection = selection
self.items = items
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateControllerLifecycle(from oldValue: Items.Element?, to newValue: Items.Element?) {
switch (oldValue, newValue) {
case (.none, .some):
presentController()
case (.some, .some):
updateController()
case (.some, .none):
dismissController()
case (.none, .none):
break
}
}
private func presentController() {
print("Present")
let controller = QLPreviewController(nibName: nil, bundle: nil)
controller.dataSource = self
controller.delegate = self
present(controller, animated: true)
updateController()
}
private func updateController() {
let controller = presentedViewController as? QLPreviewController
controller?.reloadData()
let index = selection.wrappedValue.flatMap { items.firstIndex(of: $0) }
controller?.currentPreviewItemIndex = items.distance(from: items.startIndex, to: index ?? items.startIndex)
}
private func dismissController() {
DispatchQueue.main.async {
self.selection.wrappedValue = nil
}
}
func numberOfPreviewItems(in _: QLPreviewController) -> Int {
items.isEmpty ? 1 : items.count
}
func previewController(_: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
if items.isEmpty {
return (selection.wrappedValue ?? URL(fileURLWithPath: "")) as NSURL
} else {
let index = items.index(items.startIndex, offsetBy: index)
return items[index] as NSURL
}
}
func previewControllerDidDismiss(_: QLPreviewController) {
dismissController()
}
}
#endif

View File

@ -1,115 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(macOS)
import QuickLook
import QuickLookUI
@available(macOS 10.15, *)
final class PreviewController<Items>: NSViewController, QLPreviewPanelDataSource, QLPreviewPanelDelegate
where Items: RandomAccessCollection, Items.Element == URL
{
private let panel = QLPreviewPanel.shared()!
private weak var windowResponder: NSResponder?
var items: Items
var selection: Binding<Items.Element?> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: selection.wrappedValue
)
}
}
private func updateControllerLifecycle(from oldValue: Items.Element?, to newValue: Items.Element?) {
switch (oldValue, newValue) {
case (.none, .some):
present()
case (.some, .some):
update()
case (.some, .none):
dismiss()
case (.none, .none):
break
}
}
init(selection: Binding<Items.Element?>, in items: Items) {
self.selection = selection
self.items = items
super.init(nibName: nil, bundle: nil)
windowResponder = NSApp.mainWindow?.nextResponder
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = .init(frame: .zero)
}
var isVisible: Bool {
QLPreviewPanel.sharedPreviewPanelExists() && panel.isVisible
}
private func present() {
print("Present")
NSApp.mainWindow?.nextResponder = self
if isVisible {
panel.updateController()
let index = selection.wrappedValue.flatMap { items.firstIndex(of: $0) }
panel.currentPreviewItemIndex = items.distance(from: items.startIndex, to: index ?? items.startIndex)
} else {
panel.makeKeyAndOrderFront(nil)
}
}
private func update() {
present()
}
private func dismiss() {
selection.wrappedValue = nil
}
func numberOfPreviewItems(in _: QLPreviewPanel!) -> Int {
items.isEmpty ? 1 : items.count
}
func previewPanel(_: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
if items.isEmpty {
return selection.wrappedValue as? NSURL
} else {
let index = items.index(items.startIndex, offsetBy: index)
return items[index] as NSURL
}
}
override func acceptsPreviewPanelControl(_: QLPreviewPanel!) -> Bool {
print("Accept")
return true
}
override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) {
print("Begin")
panel.dataSource = self
panel.reloadData()
}
override func endPreviewPanelControl(_ panel: QLPreviewPanel!) {
print("End")
panel.dataSource = nil
dismiss()
}
}
#endif

View File

@ -1,101 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if canImport(QuickLook)
import QuickLook
#endif
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
@available(macOS 10.15, *)
extension Backport where Wrapped: View {
/// Presents a Quick Look preview of the URLs you provide.
///
/// The Quick Look preview appears when you set the binding to a non-`nil` item.
/// When you set the item back to `nil`, Quick Look dismisses the preview.
/// If the value of the selection binding isnt contained in the items collection, Quick Look treats it the same as a `nil` selection.
///
/// Quick Look updates the value of the selection binding to match the URL of the file the user is previewing.
/// Upon dismissal by the user, Quick Look automatically sets the item binding to `nil`.
///
/// - Parameters:
/// - selection: A <doc://com.apple.documentation/documentation/SwiftUI/Binding> to an element thats part of the items collection. This is the URL that you currently want to preview.
/// - items: A collection of URLs to preview.
///
/// - Returns: A view that presents the preview of the contents of the URL.
public func quickLookPreview<Items>(_ selection: Binding<Items.Element?>, in items: Items) -> some View
where Items: RandomAccessCollection, Items.Element == URL
{
#if os(iOS) || os(macOS)
content.background(QuicklookSheet(selection: selection, items: items))
#else
content
#endif
}
/// Presents a Quick Look preview of the contents of a single URL.
///
/// The Quick Look preview appears when you set the binding to a non-`nil` item.
/// When you set the item back to `nil`, Quick Look dismisses the preview.
///
/// Upon dismissal by the user, Quick Look automatically sets the item binding to `nil`.
/// Quick Look displays the preview when a non-`nil` item is set.
/// Set `item` to `nil` to dismiss the preview.
///
/// - Parameters:
/// - item: A <doc://com.apple.documentation/documentation/SwiftUI/Binding> to a URL that should be previewed.
///
/// - Returns: A view that presents the preview of the contents of the URL.
public func quickLookPreview(_ item: Binding<URL?>) -> some View {
#if os(iOS) || os(macOS)
content.background(QuicklookSheet(selection: item, items: [item.wrappedValue].compactMap { $0 }))
#else
content
#endif
}
}
#if os(macOS)
import QuickLookUI
@available(macOS 10.15, *)
private struct QuicklookSheet<Items>: NSViewControllerRepresentable
where Items: RandomAccessCollection, Items.Element == URL
{
let selection: Binding<Items.Element?>
let items: Items
func makeNSViewController(context _: Context) -> PreviewController<Items> {
.init(selection: selection, in: items)
}
func updateNSViewController(_ controller: PreviewController<Items>, context _: Context) {
controller.selection = selection
controller.items = items
}
}
#elseif os(iOS)
private struct QuicklookSheet<Items>: UIViewControllerRepresentable
where Items: RandomAccessCollection, Items.Element == URL
{
let selection: Binding<Items.Element?>
let items: Items
func makeUIViewController(context _: Context) -> PreviewController<Items> {
.init(selection: selection, in: items)
}
func updateUIViewController(_ controller: PreviewController<Items>, context _: Context) {
controller.items = items
controller.selection = selection
}
}
#endif

View File

@ -1,193 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped: View {
/// Marks this view as refreshable.
///
/// Apply this modifier to a view to set the ``EnvironmentValues/refresh``
/// value in the view's environment to a ``RefreshAction`` instance that
/// uses the specified `action` as its handler. Views that detect the
/// presence of the instance can change their appearance to provide a
/// way for the user to execute the handler.
///
/// You can add refresh capability to your own views as well. For
/// information on how to do that, see ``RefreshAction``.
///
/// - Parameters:
/// - action: An asynchronous handler that SwiftUI executes when the
/// user requests a refresh. Use this handler to initiate
/// an update of model data displayed in the modified view. Use
/// `await` in front of any asynchronous calls inside the handler.
/// - Returns: A view with a new refresh action in its environment.
public func refreshable(action: @escaping @Sendable () async -> Void) -> some View {
#if os(iOS)
content
.environment(\.backportRefresh, Backport<Any>.RefreshAction(action))
.inspect { inspector in
inspector.sibling(ofType: UITableView.self)
} customize: { scrollView in
guard scrollView.refreshControl == nil else { return }
scrollView.refreshControl = RefreshControl {
await action()
}
}
#else
content
.environment(\.backportRefresh, Backport<Any>.RefreshAction(action))
#endif
}
}
#if os(iOS)
private final class RefreshControl: UIRefreshControl {
var handler: (() async -> Void)?
init(_ handler: @escaping () async -> Void) {
super.init()
self.handler = { [weak self] in
Task { [weak self] in
await handler()
self?.endRefreshing()
}
}
addTarget(self, action: #selector(update), for: .valueChanged)
}
@MainActor
override func endRefreshing() {
super.endRefreshing()
}
@objc private func update() {
Task { await handler?() }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// An action that initiates a refresh operation.
///
/// Unlike the official implementation, this backport does not affect any
/// view's like `List` to provide automatic pull-to-refresh behaviour.
///
/// You can use this to offer refresh capability in your custom views.
/// Read the ``EnvironmentValues/refresh`` environment value to get the
/// `RefreshAction` instance for a given ``Environment``. If you find
/// a non-`nil` value, change your view's appearance or behavior to offer
/// the refresh to the user, and call the instance to conduct the
/// refresh. You can call the refresh instance directly because it defines
/// a ``RefreshAction/callAsFunction()`` method that Swift calls
/// when you call the instance:
///
/// struct RefreshableView: View {
/// @Environment(\.refresh) private var refresh
///
/// var body: some View {
/// Button("Refresh") {
/// Task {
/// await refresh?()
/// }
/// }
/// .disabled(refresh == nil)
/// }
/// }
///
/// Be sure to call the handler asynchronously by preceding it
/// with `await`. Because the call is asynchronous, you can use
/// its lifetime to indicate progress to the user. For example,
/// you might reveal an indeterminate ``ProgressView`` before
/// calling the handler, and hide it when the handler completes.
///
/// If your code isn't already in an asynchronous context, create a
/// <doc://com.apple.documentation/documentation/Swift/Task> for the
/// method to run in. If you do this, consider adding a way for the
/// user to cancel the task. For more information, see
/// [Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
/// in *The Swift Programming Language*.
public struct RefreshAction {
private var action: () async -> Void
internal init(_ action: @escaping () async -> Void) {
self.action = action
}
public func callAsFunction() async {
await action()
}
}
}
@available(macOS 10.15, *)
private struct RefreshEnvironmentKey: EnvironmentKey {
static let defaultValue: Backport<Any>.RefreshAction? = nil
}
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
@available(macOS 10.15, *)
extension EnvironmentValues {
/// An action that initiates a refresh operation.
///
/// Unlike the official implementation, this backport does not affect any
/// view's like `List` to provide automatic pull-to-refresh behaviour.
///
/// You can use this to offer refresh capability in your custom views.
/// Read the ``EnvironmentValues/refresh`` environment value to get the
/// `RefreshAction` instance for a given ``Environment``. If you find
/// a non-`nil` value, change your view's appearance or behavior to offer
/// the refresh to the user, and call the instance to conduct the
/// refresh. You can call the refresh instance directly because it defines
/// a ``RefreshAction/callAsFunction()`` method that Swift calls
/// when you call the instance:
///
/// struct RefreshableView: View {
/// @Environment(\.refresh) private var refresh
///
/// var body: some View {
/// Button("Refresh") {
/// Task {
/// await refresh?()
/// }
/// }
/// .disabled(refresh == nil)
/// }
/// }
///
/// Be sure to call the handler asynchronously by preceding it
/// with `await`. Because the call is asynchronous, you can use
/// its lifetime to indicate progress to the user. For example,
/// you might reveal an indeterminate ``ProgressView`` before
/// calling the handler, and hide it when the handler completes.
///
/// If your code isn't already in an asynchronous context, create a
/// <doc://com.apple.documentation/documentation/Swift/Task> for the
/// method to run in. If you do this, consider adding a way for the
/// user to cancel the task. For more information, see
/// [Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
/// in *The Swift Programming Language*.
public var backportRefresh: Backport<Any>.RefreshAction? {
get { self[RefreshEnvironmentKey.self] }
set { self[RefreshEnvironmentKey.self] = newValue }
}
}

View File

@ -1,43 +0,0 @@
import StoreKit
import SwiftUI
#if os(iOS) || os(macOS)
@available(macOS 10.15, *)
public extension EnvironmentValues {
/// An instance that tells StoreKit to request an App Store rating or review from the user, if appropriate.
/// Read the requestReview environment value to get an instance of this structure for a given Environment. Call the instance to tell StoreKit to ask the user to rate or review your app, if appropriate. You call the instance directly because it defines a callAsFunction() method that Swift calls when you call the instance.
///
/// Although you normally call this instance to request a review when it makes sense in the user experience flow of your app, the App Store policy governs the actual display of the rating and review request view. Because calling this instance may not present an alert, dont call it in response to a user action, such as a button tap.
///
/// > When you call this instance while your app is in development mode, the system always displays a rating and review request view so you can test the user interface and experience. This instance has no effect when you call it in an app that you distribute using TestFlight.
@MainActor var backportRequestReview: Backport<Any>.RequestReviewAction { .init() }
}
/// An instance that tells StoreKit to request an App Store rating or review from the user, if appropriate.
/// Read the requestReview environment value to get an instance of this structure for a given Environment. Call the instance to tell StoreKit to ask the user to rate or review your app, if appropriate. You call the instance directly because it defines a callAsFunction() method that Swift calls when you call the instance.
///
/// Although you normally call this instance to request a review when it makes sense in the user experience flow of your app, the App Store policy governs the actual display of the rating and review request view. Because calling this instance may not present an alert, dont call it in response to a user action, such as a button tap.
///
/// > When you call this instance while your app is in development mode, the system always displays a rating and review request view so you can test the user interface and experience. This instance has no effect when you call it in an app that you distribute using TestFlight.
///
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(macOS, deprecated: 13)
extension Backport where Wrapped == Any {
@MainActor public struct RequestReviewAction {
public func callAsFunction() {
#if os(macOS)
SKStoreReviewController.requestReview()
#else
if #available(iOS 14, *) {
guard let scene = UIApplication.activeScene else { return }
SKStoreReviewController.requestReview(in: scene)
} else {
SKStoreReviewController.requestReview()
}
#endif
}
}
}
#endif

View File

@ -1,58 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped == Any {
/// A container view that you can use to add hierarchy to certain collection views.
///
/// Use `Section` instances in views like ``List``, ``Picker``, and
/// ``Form`` to organize content into separate sections. Each section has
/// custom content that you provide on a per-instance basis. You can also
/// provide headers and footers for each section.
public struct Section<Parent: View, Content: View, Footer: View>: View {
@ViewBuilder let content: () -> Content
@ViewBuilder let header: () -> Parent
@ViewBuilder let footer: () -> Footer
@available(macOS 10.15, *)
public var body: some View {
SwiftUI.Section(
content: content,
header: header,
footer: footer
)
}
}
}
@available(macOS 10.15, *)
public extension Backport.Section where Wrapped == Any, Parent == Text, Footer == EmptyView {
/// Creates a section with the provided section content.
/// - Parameters:
/// - titleKey: The key for the section's localized title, which describes
/// the contents of the section.
/// - content: The section's content.
init(_ titleKey: LocalizedStringKey, @ViewBuilder content: @escaping () -> Content) {
header = { Text(titleKey) }
self.content = content
footer = { EmptyView() }
}
@available(macOS 10.15, *)
/// Creates a section with the provided section content.
/// - Parameters:
/// - title: A string that describes the contents of the section.
/// - content: The section's content.
init<S>(_ title: S, @ViewBuilder content: @escaping () -> Content) where S: StringProtocol {
header = { Text(title) }
self.content = content
footer = { EmptyView() }
}
}

View File

@ -1,159 +0,0 @@
import Combine
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14.0)
@available(macOS, deprecated: 11.0)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped: ObservableObject {
/// A property wrapper type that instantiates an observable object.
///
/// Create a state object in a ``SwiftUI/View``, ``SwiftUI/App``, or
/// ``SwiftUI/Scene`` by applying the `@Backport.StateObject` attribute to a property
/// declaration and providing an initial value that conforms to the
/// <doc://com.apple.documentation/documentation/Combine/ObservableObject>
/// protocol:
///
/// @Backport.StateObject var model = DataModel()
///
/// SwiftUI creates a new instance of the object only once for each instance of
/// the structure that declares the object. When published properties of the
/// observable object change, SwiftUI updates the parts of any view that depend
/// on those properties:
///
/// Text(model.title) // Updates the view any time `title` changes.
///
/// You can pass the state object into a property that has the
/// ``SwiftUI/ObservedObject`` attribute. You can alternatively add the object
/// to the environment of a view hierarchy by applying the
/// ``SwiftUI/View/environmentObject(_:)`` modifier:
///
/// ContentView()
/// .environmentObject(model)
///
/// If you create an environment object as shown in the code above, you can
/// read the object inside `ContentView` or any of its descendants
/// using the ``SwiftUI/EnvironmentObject`` attribute:
///
/// @EnvironmentObject var model: DataModel
///
/// Get a ``SwiftUI/Binding`` to one of the state object's properties using the
/// `$` operator. Use a binding when you want to create a two-way connection to
/// one of the object's properties. For example, you can let a
/// ``SwiftUI/Toggle`` control a Boolean value called `isEnabled` stored in the
/// model:
///
/// Toggle("Enabled", isOn: $model.isEnabled)
@propertyWrapper public struct StateObject: DynamicProperty {
private final class Wrapper: ObservableObject {
private var subject = PassthroughSubject<Void, Never>()
@available(macOS 10.15, *)
var value: Wrapped? {
didSet {
cancellable = nil
cancellable = value?.objectWillChange
.sink { [subject] _ in subject.send() }
}
}
@available(macOS 10.15, *)
private var cancellable: AnyCancellable?
@available(macOS 10.15, *)
var objectWillChange: AnyPublisher<Void, Never> {
subject.eraseToAnyPublisher()
}
}
@available(macOS 10.15, *)
@State private var state = Wrapper()
@available(macOS 10.15, *)
@ObservedObject private var observedObject = Wrapper()
@available(macOS 10.15, *)
private var thunk: () -> Wrapped
@available(macOS 10.15, *)
/// The underlying value referenced by the state object.
///
/// The wrapped value property provides primary access to the value's data.
/// However, you don't access `wrappedValue` directly. Instead, use the
/// property variable created with the `@Backport.StateObject` attribute:
///
/// @Backport.StateObject var contact = Contact()
///
/// var body: some View {
/// Text(contact.name) // Accesses contact's wrapped value.
/// }
///
/// When you change a property of the wrapped value, you can access the new
/// value immediately. However, SwiftUI updates views displaying the value
/// asynchronously, so the user interface might not update immediately.
public var wrappedValue: Wrapped {
if let object = state.value {
return object
} else {
let object = thunk()
state.value = object
return object
}
}
@available(macOS 10.15, *)
/// A projection of the state object that creates bindings to its
/// properties.
///
/// Use the projected value to pass a binding value down a view hierarchy.
/// To get the projected value, prefix the property variable with `$`. For
/// example, you can get a binding to a model's `isEnabled` Boolean so that
/// a ``SwiftUI/Toggle`` view can control the value:
///
/// struct MyView: View {
/// @Backport.StateObject var model = DataModel()
///
/// var body: some View {
/// Toggle("Enabled", isOn: $model.isEnabled)
/// }
/// }
public var projectedValue: ObservedObject<Wrapped>.Wrapper {
ObservedObject(wrappedValue: wrappedValue).projectedValue
}
@available(macOS 10.15, *)
/// Creates a new state object with an initial wrapped value.
///
/// You dont call this initializer directly. Instead, declare a property
/// with the `@Backport.StateObject` attribute in a ``SwiftUI/View``,
/// ``SwiftUI/App``, or ``SwiftUI/Scene``, and provide an initial value:
///
/// struct MyView: View {
/// @Backport.StateObject var model = DataModel()
///
/// // ...
/// }
///
/// SwiftUI creates only one instance of the state object for each
/// container instance that you declare. In the code above, SwiftUI
/// creates `model` only the first time it initializes a particular instance
/// of `MyView`. On the other hand, each different instance of `MyView`
/// receives a distinct copy of the data model.
///
/// - Parameter thunk: An initial value for the state object.
public init(wrappedValue thunk: @autoclosure @escaping () -> Wrapped) {
self.thunk = thunk
}
@available(macOS 10.15, *)
public mutating func update() {
if state.value == nil {
state.value = thunk()
}
if observedObject.value !== state.value {
observedObject.value = state.value
}
}
}
}

View File

@ -1,178 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15.0)
@available(macOS, deprecated: 12.0)
@available(tvOS, deprecated: 15.0)
@available(watchOS, deprecated: 8.0)
extension Backport where Wrapped: View {
/// Adds an asynchronous task to perform when this view appears.
///
/// Use this modifier to perform an asynchronous task with a lifetime that
/// matches that of the modified view. If the task doesn't finish
/// before SwiftUI removes the view or the view changes identity, SwiftUI
/// cancels the task.
///
/// Use the `await` keyword inside the task to
/// wait for an asynchronous call to complete.
///
/// let url = URL(string: "https://example.com")!
/// @State private var message = "Loading..."
///
/// var body: some View {
/// Text(message)
/// .task {
/// do {
/// var receivedLines = [String]()
/// for try await line in url.lines {
/// receivedLines.append(line)
/// message = "Received \(receivedLines.count) lines"
/// }
/// } catch {
/// message = "Failed to load"
/// }
/// }
/// }
///
/// When each new line arrives, the body of the `for`-`await`-`in`
/// loop stores the line in an array of strings and updates the content of the
/// text view to report the latest line count.
///
/// - Parameters:
/// - priority: The task priority to use when creating the asynchronous
/// task. The default priority is `.userInitiated`
/// - action: A closure that SwiftUI calls as an asynchronous task
/// when the view appears. SwiftUI automatically cancels the task
/// if the view disappears before the action completes.
///
///
/// - Returns: A view that runs the specified action asynchronously when
/// the view appears.
@ViewBuilder
public func task(priority: TaskPriority = .userInitiated, _ action: @MainActor @escaping @Sendable () async -> Void)
-> some View
{
content.modifier(
TaskModifier(
id: 0,
priority: priority,
action: action
)
)
}
@available(macOS 10.15, *)
/// Adds a task to perform when this view appears or when a specified
/// value changes.
///
/// This method behaves like ``View/task(priority:_:)``, except that it also
/// cancels and recreates the task when a specified value changes. To detect
/// a change, the modifier tests whether a new value for the `id` parameter
/// equals the previous value. For this to work,
/// the value's type must conform to the `Equatable` protocol.
///
/// For example, if you define an equatable `Server` type that posts custom
/// notifications whenever its state changes --- for example, from _signed
/// out_ to _signed in_ --- you can use the task modifier to update
/// the contents of a ``Text`` view to reflect the state of the
/// currently selected server:
///
/// Text(status ?? "Signed Out")
/// .task(id: server) {
/// let sequence = NotificationCenter.default.notifications(
/// named: .didChangeStatus,
/// object: server)
/// for try await notification in sequence {
/// status = notification.userInfo["status"] as? String
/// }
/// }
///
/// Elsewhere, the server defines a custom `didUpdateStatus` notification:
///
/// extension NSNotification.Name {
/// static var didUpdateStatus: NSNotification.Name {
/// NSNotification.Name("didUpdateStatus")
/// }
/// }
///
/// The server then posts a notification of this type whenever its status
/// changes, like after the user signs in:
///
/// let notification = Notification(
/// name: .didUpdateStatus,
/// object: self,
/// userInfo: ["status": "Signed In"])
/// NotificationCenter.default.post(notification)
///
/// The task attached to the ``Text`` view gets and displays the status
/// value from the notification's user information dictionary. When the user
/// chooses a different server, SwiftUI cancels the task and creates a new
/// one, which then starts waiting for notifications from the new server.
///
/// - Parameters:
/// - id: The value to observe for changes. The value must conform
/// to the `Equatable` protocol.
/// - priority: The task priority to use when creating the asynchronous
/// task. The default priority is `.userInitiated`
/// - action: A closure that SwiftUI calls as an asynchronous task
/// when the view appears. SwiftUI automatically cancels the task
/// if the view disappears before the action completes. If the
/// `id` value changes, SwiftUI cancels and restarts the task.
///
/// - Returns: A view that runs the specified action asynchronously when
/// the view appears, or restarts the task with the `id` value changes.
@ViewBuilder
public func task<T: Equatable>(
id: T, priority: TaskPriority = .userInitiated, _ action: @MainActor @escaping @Sendable () async -> Void
) -> some View {
content.modifier(
TaskModifier(
id: id,
priority: priority,
action: action
)
)
}
}
@available(macOS 10.15, *)
private struct TaskModifier<ID: Equatable>: ViewModifier {
var id: ID
var priority: TaskPriority
var action: () async -> Void
@available(macOS 10.15, *)
@State private var task: Task<Void, Never>?
@available(macOS 10.15, *)
init(id: ID, priority: TaskPriority, action: @MainActor @escaping () async -> Void) {
self.id = id
self.priority = priority
self.action = action
}
@available(macOS 10.15, *)
func body(content: Content) -> some View {
content
.backport.onChange(of: id) { _ in
task?.cancel()
task = Task(priority: priority) {
await action()
}
}
.onAppear {
task?.cancel()
task = Task(priority: priority) {
await action()
}
}
.onDisappear {
task?.cancel()
task = nil
}
}
}

View File

@ -1,34 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped == AnyTransition {
/// Creates a transition that when added to a view will animate the views insertion by moving it in from the specified edge while fading it in, and animate its removal by moving it out towards the opposite edge and fading it out.
/// - Parameter edge: the edge from which the view will be animated in.
/// - Returns: A transition that animates a view by moving and fading it.
@available(iOS, deprecated: 16.0)
@available(watchOS, deprecated: 9.0)
@available(macOS, deprecated: 13.0)
@available(tvOS, deprecated: 16.0)
func push(from edge: Edge) -> AnyTransition {
var oppositeEdge: Edge
switch edge {
case .top:
oppositeEdge = .bottom
case .leading:
oppositeEdge = .trailing
case .bottom:
oppositeEdge = .top
case .trailing:
oppositeEdge = .leading
}
return .asymmetric(
insertion: .move(edge: edge),
removal: .move(edge: oppositeEdge)
).combined(with: .opacity)
}
}

View File

@ -1,168 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Foundation
@available(macOS 10.15, *)
@available(iOS, deprecated: 15.0)
@available(macOS, deprecated: 12.0)
@available(tvOS, deprecated: 15.0)
@available(watchOS, deprecated: 8.0)
extension Backport where Wrapped: URLSession {
/// Start a data task with a URL using async/await.
/// - parameter url: The URL to send a request to.
/// - returns: A tuple containing the binary `Data` that was downloaded,
/// as well as a `URLResponse` representing the server's response.
/// - throws: Any error encountered while performing the data task.
public func data(from url: URL) async throws -> (Data, URLResponse) {
try await data(for: URLRequest(url: url))
}
/// Start a data task with a `URLRequest` using async/await.
/// - parameter request: The `URLRequest` that the data task should perform.
/// - returns: A tuple containing the binary `Data` that was downloaded,
/// as well as a `URLResponse` representing the server's response.
/// - throws: Any error encountered while performing the data task.
public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.dataTask(with: request) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
continuation.resume(throwing: error)
return
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func upload(for request: URLRequest, fromFile fileURL: URL) async throws -> (Data, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.uploadTask(with: request, fromFile: fileURL) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func upload(for request: URLRequest, from bodyData: Data) async throws -> (Data, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.uploadTask(with: request, from: bodyData) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func download(for request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.downloadTask(with: request) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func download(from url: URL) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.downloadTask(with: url) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func download(resumeFrom resumeData: Data) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.downloadTask(withResumeData: resumeData) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
@available(macOS 10.15, *)
private actor URLSessionTaskActor {
weak var task: URLSessionTask?
func start(_ task: URLSessionTask) {
self.task = task
task.resume()
}
func cancel() {
task?.cancel()
}
}

View File

@ -1,42 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped == Any {
public enum Visibility: Hashable, CaseIterable {
/// The element may be visible or hidden depending on the policies of the
/// component accepting the visibility configuration.
///
/// For example, some components employ different automatic behavior
/// depending on factors including the platform, the surrounding container,
/// user settings, etc.
case automatic
/// The element may be visible.
///
/// Some APIs may use this value to represent a hint or preference, rather
/// than a mandatory assertion. For example, setting list row separator
/// visibility to `visible` using the
/// ``View/listRowSeparator(_:edges:)`` modifier may not always
/// result in any visible separators, especially for list styles that do not
/// include separators as part of their design.
case visible
/// The element may be hidden.
///
/// Some APIs may use this value to represent a hint or preference, rather
/// than a mandatory assertion. For example, setting confirmation dialog
/// title visibility to `hidden` using the
/// ``View/confirmationDialog(_:isPresented:titleVisibility:actions:)-87n66``
/// modifier may not always hide the dialog title, which is required on
/// some platforms.
case hidden
}
}

View File

@ -1,315 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped: View {
/// Sets the available detents for the enclosing sheet.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents([.medium, .large])
/// }
/// }
/// }
///
/// - Parameter detents: A set of supported detents for the sheet.
/// If you provide more than one detent, people can drag the sheet
/// to resize it.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
public func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>) -> some View {
#if os(iOS)
content.background(Backport<Any>.Representable(detents: detents, selection: nil, largestUndimmed: .large))
#else
content
#endif
}
@available(macOS 10.15, *)
/// Sets the available detents for the enclosing sheet, giving you
/// programmatic control of the currently selected detent.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
/// @State private var settingsDetent = PresentationDetent.medium
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:(
/// [.medium, .large],
/// selection: $settingsDetent
/// )
/// }
/// }
/// }
///
/// - Parameters:
/// - detents: A set of supported detents for the sheet.
/// If you provide more that one detent, people can drag the sheet
/// to resize it.
/// - selection: A ``Binding`` to the currently selected detent.
/// Ensure that the value matches one of the detents that you
/// provide for the `detents` parameter.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
public func presentationDetents(
_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>
) -> some View {
#if os(iOS)
content.background(Backport<Any>.Representable(detents: detents, selection: selection, largestUndimmed: .large))
#else
content
#endif
}
@available(macOS 10.15, *)
/// Sets the available detents for the enclosing sheet, giving you
/// programmatic control of the currently selected detent.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
/// @State private var settingsDetent = PresentationDetent.medium
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:(
/// [.medium, .large],
/// selection: $settingsDetent
/// )
/// }
/// }
/// }
///
/// - Parameters:
/// - detents: A set of supported detents for the sheet.
/// If you provide more that one detent, people can drag the sheet
/// to resize it.
/// - selection: A ``Binding`` to the currently selected detent.
/// Ensure that the value matches one of the detents that you
/// provide for the `detents` parameter.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
public func presentationDetents(
_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>,
largestUndimmedDetent: Backport<Any>.PresentationDetent? = nil
) -> some View {
#if os(iOS)
content.background(
Backport<Any>.Representable(detents: detents, selection: selection, largestUndimmed: largestUndimmedDetent))
#else
content
#endif
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped == Any {
/// A type that represents a height where a sheet naturally rests.
public struct PresentationDetent: Hashable, Comparable {
public struct Identifier: RawRepresentable, Hashable {
public var rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
@available(macOS 10.15, *)
public static var medium: Identifier {
.init(rawValue: "com.apple.UIKit.medium")
}
@available(macOS 10.15, *)
public static var large: Identifier {
.init(rawValue: "com.apple.UIKit.large")
}
}
@available(macOS 10.15, *)
public let id: Identifier
@available(macOS 10.15, *)
/// The system detent for a sheet that's approximately half the height of
/// the screen, and is inactive in compact height.
public static var medium: PresentationDetent {
.init(id: .medium)
}
@available(macOS 10.15, *)
/// The system detent for a sheet at full height.
public static var large: PresentationDetent {
.init(id: .large)
}
@available(macOS 10.15, *)
fileprivate static var none: PresentationDetent {
.init(id: .init(rawValue: ""))
}
@available(macOS 10.15, *)
public static func < (lhs: PresentationDetent, rhs: PresentationDetent) -> Bool {
switch (lhs, rhs) {
case (.large, .medium):
return false
default:
return true
}
}
}
}
#if os(iOS)
@available(iOS 15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let detents: Set<Backport<Any>.PresentationDetent>
let selection: Binding<Backport<Any>.PresentationDetent>?
let largestUndimmed: Backport<Any>.PresentationDetent?
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(detents: detents, selection: selection, largestUndimmed: largestUndimmed)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(detents: detents, selection: selection, largestUndimmed: largestUndimmed)
}
}
}
@available(macOS 10.15, *)
@available(iOS 15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController, UISheetPresentationControllerDelegate {
var detents: Set<Backport<Any>.PresentationDetent>
var selection: Binding<Backport<Any>.PresentationDetent>?
var largestUndimmed: Backport<Any>.PresentationDetent?
weak var _delegate: UISheetPresentationControllerDelegate?
@available(macOS 10.15, *)
init(
detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?,
largestUndimmed: Backport<Any>.PresentationDetent?
) {
self.detents = detents
self.selection = selection
self.largestUndimmed = largestUndimmed
super.init(nibName: nil, bundle: nil)
}
@available(macOS 10.15, *)
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(macOS 10.15, *)
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if let controller = parent?.sheetPresentationController {
if controller.delegate !== self {
_delegate = controller.delegate
controller.delegate = self
}
}
update(detents: detents, selection: selection, largestUndimmed: largestUndimmed)
}
@available(macOS 10.15, *)
func update(
detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?,
largestUndimmed: Backport<Any>.PresentationDetent?
) {
self.detents = detents
self.selection = selection
self.largestUndimmed = largestUndimmed
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.detents = detents.sorted().map {
switch $0 {
case .medium:
return .medium()
default:
return .large()
}
}
controller.largestUndimmedDetentIdentifier = largestUndimmed.flatMap {
.init(rawValue: $0.id.rawValue)
}
if let selection = selection {
controller.selectedDetentIdentifier = .init(selection.wrappedValue.id.rawValue)
}
controller.prefersScrollingExpandsWhenScrolledToEdge = true
}
UIView.animate(withDuration: 0.25) {
if let undimmed = largestUndimmed {
controller.presentingViewController.view.tintAdjustmentMode =
(selection?.wrappedValue ?? .large) >= undimmed ? .automatic : .normal
} else {
controller.presentingViewController.view.tintAdjustmentMode = .automatic
}
}
}
}
@available(macOS 10.15, *)
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
_ sheetPresentationController: UISheetPresentationController
) {
guard
let selection = selection,
let id = sheetPresentationController.selectedDetentIdentifier?.rawValue,
selection.wrappedValue.id.rawValue != id
else { return }
selection.wrappedValue = .init(id: .init(rawValue: id))
}
@available(macOS 10.15, *)
override func responds(to aSelector: Selector!) -> Bool {
if super.responds(to: aSelector) { return true }
if _delegate?.responds(to: aSelector) ?? false { return true }
return false
}
@available(macOS 10.15, *)
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if super.responds(to: aSelector) { return self }
return _delegate
}
}
}
#endif

View File

@ -1,102 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped: View {
/// Sets the visibility of the drag indicator on top of a sheet.
///
/// You can show a drag indicator when it isn't apparent that a
/// sheet can resize or when the sheet can't dismiss interactively.
///
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:([.medium, .large])
/// .presentationDragIndicator(.visible)
/// }
/// }
/// }
///
/// - Parameter visibility: The preferred visibility of the drag indicator.
@ViewBuilder
public func presentationDragIndicator(_ visibility: Backport<Any>.Visibility) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(visibility: visibility))
} else {
content
}
#else
content
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
@available(macOS 10.15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let visibility: Backport<Any>.Visibility
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(visibility: visibility)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(visibility: visibility)
}
}
}
@available(macOS 10.15, *)
@available(iOS 15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController {
var visibility: Backport<Any>.Visibility
@available(macOS 10.15, *)
init(visibility: Backport<Any>.Visibility) {
self.visibility = visibility
super.init(nibName: nil, bundle: nil)
}
@available(macOS 10.15, *)
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(macOS 10.15, *)
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(visibility: visibility)
}
@available(macOS 10.15, *)
func update(visibility: Backport<Any>.Visibility) {
self.visibility = visibility
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.prefersGrabberVisible = visibility == .visible
controller.prefersScrollingExpandsWhenScrolledToEdge = true
}
}
}
}
}
#endif

View File

@ -1,104 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
@available(macOS 10.15, *)
extension Backport where Wrapped: View {
/// Removes dimming from detents higher (and including) the provided identifier
///
/// This has two affects on dentents higher than the identifier provided:
/// 1. Touches will passthrough to the views below the sheet.
/// 2. Touches will no longer dismiss the sheet automatically when tapping outside of the sheet.
///
/// ```
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:([.medium, .large])
/// .presentationUndimmed(from: .medium)
/// }
/// }
/// }
/// ```
///
/// - Parameter identifier: The identifier of the largest detent that is not dimmed.
@ViewBuilder
@available(
iOS, deprecated: 13, message: "Please use backport.presentationDetents(_:selection:largestUndimmedDetent:)"
)
public func presentationUndimmed(from identifier: Backport<Any>.PresentationDetent.Identifier?) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(identifier: identifier))
} else {
content
}
#else
content
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let identifier: Backport<Any>.PresentationDetent.Identifier?
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(identifier: identifier)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(identifier: identifier)
}
}
}
@available(iOS 15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController {
var identifier: Backport<Any>.PresentationDetent.Identifier?
init(identifier: Backport<Any>.PresentationDetent.Identifier?) {
self.identifier = identifier
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(identifier: identifier)
}
func update(identifier: Backport<Any>.PresentationDetent.Identifier?) {
self.identifier = identifier
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.presentingViewController.view.tintAdjustmentMode = .normal
controller.largestUndimmedDetentIdentifier = identifier.flatMap {
.init(rawValue: $0.rawValue)
}
}
}
}
}
}
#endif

View File

@ -1,256 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Conditionally prevents interactive dismissal of a popover or a sheet.
///
/// Users can dismiss certain kinds of presentations using built-in
/// gestures. In particular, a user can dismiss a sheet by dragging it down,
/// or a popover by clicking or tapping outside of the presented view. Use
/// the `interactiveDismissDisabled(_:)` modifier to conditionally prevent
/// this kind of dismissal. You typically do this to prevent the user from
/// dismissing a presentation before providing needed data or completing
/// a required action.
///
/// For instance, suppose you have a view that displays a licensing
/// agreement that the user must acknowledge before continuing:
///
/// struct TermsOfService: View {
/// @Binding var areTermsAccepted: Bool
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Form {
/// Text("License Agreement")
/// .font(.title)
/// Text("Terms and conditions go here.")
/// Button("Accept") {
/// areTermsAccepted = true
/// dismiss()
/// }
/// }
/// }
/// }
///
/// If you present this view in a sheet, the user can dismiss it by either
/// tapping the button --- which calls ``EnvironmentValues/backportDismiss``
/// from its `action` closure --- or by dragging the sheet down. To
/// ensure that the user accepts the terms by tapping the button,
/// disable interactive dismissal, conditioned on the `areTermsAccepted`
/// property:
///
/// struct ContentView: View {
/// @State private var isSheetPresented = false
/// @State private var areTermsAccepted = false
///
/// var body: some View {
/// Button("Use Service") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// TermsOfService()
/// .backport.interactiveDismissDisabled(!areTermsAccepted)
/// }
/// }
/// }
///
/// You can apply the modifier to any view in the sheet's view hierarchy,
/// including to the sheet's top level view, as the example demonstrates,
/// or to any child view, like the ``Form`` or the Accept ``Button``.
///
/// The modifier has no effect on programmatic dismissal, which you can
/// invoke by updating the ``Binding`` that controls the presentation, or
/// by calling the environment's ``EnvironmentValues/backportDismiss`` action.
///
/// > This modifier currently has no effect on macOS, tvOS or watchOS.
///
/// - Parameter isDisabled: A Boolean value that indicates whether to
/// prevent nonprogrammatic dismissal of the containing view hierarchy
/// when presented in a sheet or popover.
@ViewBuilder
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
} else {
content
}
#else
content
#endif
}
@available(macOS 10.15, *)
/// Conditionally prevents interactive dismissal of a popover or a sheet. In addition, provides fine-grained control over the dismissal
///
/// Users can dismiss certain kinds of presentations using built-in
/// gestures. In particular, a user can dismiss a sheet by dragging it down,
/// or a popover by clicking or tapping outside of the presented view. Use
/// the `interactiveDismissDisabled(_:)` modifier to conditionally prevent
/// this kind of dismissal. You typically do this to prevent the user from
/// dismissing a presentation before providing needed data or completing
/// a required action.
///
/// For instance, suppose you have a view that displays a licensing
/// agreement that the user must acknowledge before continuing:
///
/// struct TermsOfService: View {
/// @Binding var areTermsAccepted: Bool
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Form {
/// Text("License Agreement")
/// .font(.title)
/// Text("Terms and conditions go here.")
/// Button("Accept") {
/// areTermsAccepted = true
/// dismiss()
/// }
/// }
/// }
/// }
///
/// If you present this view in a sheet, the user can dismiss it by either
/// tapping the button --- which calls ``EnvironmentValues/backportDismiss``
/// from its `action` closure --- or by dragging the sheet down. To
/// ensure that the user accepts the terms by tapping the button,
/// disable interactive dismissal, conditioned on the `areTermsAccepted`
/// property:
///
/// struct ContentView: View {
/// @State private var isSheetPresented = false
/// @State private var areTermsAccepted = false
///
/// var body: some View {
/// Button("Use Service") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// TermsOfService()
/// .backport.interactiveDismissDisabled(!areTermsAccepted)
/// }
/// }
/// }
///
/// You can apply the modifier to any view in the sheet's view hierarchy,
/// including to the sheet's top level view, as the example demonstrates,
/// or to any child view, like the ``Form`` or the Accept ``Button``.
///
/// The modifier has no effect on programmatic dismissal, which you can
/// invoke by updating the ``Binding`` that controls the presentation, or
/// by calling the environment's ``EnvironmentValues/backportDismiss`` action.
///
/// > This modifier currently has no effect on macOS, tvOS or watchOS.
///
/// - Parameter isDisabled: A Boolean value that indicates whether to
/// prevent nonprogrammatic dismissal of the containing view hierarchy
/// when presented in a sheet or popover.
/// - Parameter onAttempt: A closure that will be called when an interactive dismiss attempt occurs.
/// You can use this as an opportunity to present an confirmation or prompt to the user.
@ViewBuilder
func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
} else {
content
}
#else
content
#endif
}
}
#if os(iOS)
@available(macOS 10.15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let isModal: Bool
let onAttempt: (() -> Void)?
@available(macOS 10.15, *)
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(isModal: isModal, onAttempt: onAttempt)
}
@available(macOS 10.15, *)
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(isModal: isModal, onAttempt: onAttempt)
}
}
}
@available(macOS 10.15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController, UIAdaptivePresentationControllerDelegate {
var isModal: Bool
var onAttempt: (() -> Void)?
weak var _delegate: UIAdaptivePresentationControllerDelegate?
@available(macOS 10.15, *)
init(isModal: Bool, onAttempt: (() -> Void)?) {
self.isModal = isModal
self.onAttempt = onAttempt
super.init(nibName: nil, bundle: nil)
}
@available(macOS 10.15, *)
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(macOS 10.15, *)
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if let controller = parent?.presentationController {
if controller.delegate !== self {
_delegate = controller.delegate
controller.delegate = self
}
}
update(isModal: isModal, onAttempt: onAttempt)
}
@available(macOS 10.15, *)
func update(isModal: Bool, onAttempt: (() -> Void)?) {
self.isModal = isModal
self.onAttempt = onAttempt
parent?.isModalInPresentation = isModal
}
@available(macOS 10.15, *)
func presentationControllerDidAttemptToDismiss(_: UIPresentationController) {
onAttempt?()
}
@available(macOS 10.15, *)
func presentationControllerShouldDismiss(_: UIPresentationController) -> Bool {
parent?.isModalInPresentation == false
}
@available(macOS 10.15, *)
override func responds(to aSelector: Selector!) -> Bool {
if super.responds(to: aSelector) { return true }
if _delegate?.responds(to: aSelector) ?? false { return true }
return false
}
@available(macOS 10.15, *)
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if super.responds(to: aSelector) { return self }
return _delegate
}
}
}
#endif

View File

@ -1,81 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A dynamic property that scales a numeric value.
@propertyWrapper
public struct ScaledMetric<Value>: DynamicProperty where Value: BinaryFloatingPoint {
@Environment(\.sizeCategory) var sizeCategory
private let baseValue: Value
#if os(iOS) || os(tvOS)
private let metrics: UIFontMetrics
#endif
public var wrappedValue: Value {
#if os(iOS) || os(tvOS)
let traits = UITraitCollection(traitsFrom: [
UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory(sizeCategory: sizeCategory)),
])
return Value(metrics.scaledValue(for: CGFloat(baseValue), compatibleWith: traits))
#else
return baseValue
#endif
}
#if os(iOS) || os(tvOS)
/// Creates the scaled metric with an unscaled value using the default scaling.
public init(baseValue: Value, metrics: UIFontMetrics) {
self.baseValue = baseValue
self.metrics = metrics
}
/// Creates the scaled metric with an unscaled value using the default scaling.
public init(wrappedValue: Value) {
self.init(baseValue: wrappedValue, metrics: UIFontMetrics(forTextStyle: .body))
}
/// Creates the scaled metric with an unscaled value and a text style to scale relative to.
public init(wrappedValue: Value, relativeTo textStyle: UIFont.TextStyle) {
self.init(baseValue: wrappedValue, metrics: UIFontMetrics(forTextStyle: textStyle))
}
#else
/// Creates the scaled metric with an unscaled value using the default scaling.
public init(wrappedValue: Value) {
baseValue = wrappedValue
}
#endif
}
}
#if os(iOS) || os(tvOS)
fileprivate extension UIContentSizeCategory {
init(sizeCategory: ContentSizeCategory?) {
switch sizeCategory {
case .accessibilityExtraExtraExtraLarge: self = .accessibilityExtraExtraExtraLarge
case .accessibilityExtraExtraLarge: self = .accessibilityExtraExtraLarge
case .accessibilityExtraLarge: self = .accessibilityExtraLarge
case .accessibilityLarge: self = .accessibilityLarge
case .accessibilityMedium: self = .accessibilityMedium
case .extraExtraExtraLarge: self = .extraExtraExtraLarge
case .extraExtraLarge: self = .extraExtraLarge
case .extraLarge: self = .extraLarge
case .extraSmall: self = .extraSmall
case .large: self = .large
case .medium: self = .medium
case .small: self = .small
default: self = .unspecified
}
}
}
#endif

View File

@ -1,69 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension EnvironmentValues {
/// The visiblity to apply to scroll indicators of any
/// vertically scrollable content.
public var backportVerticalScrollIndicatorVisibility: Backport<Any>.ScrollIndicatorVisibility {
get { self[BackportVerticalIndicatorKey.self] }
set { self[BackportVerticalIndicatorKey.self] = newValue }
}
@available(macOS 10.15, *)
/// The visibility to apply to scroll indicators of any
/// horizontally scrollable content.
public var backportHorizontalScrollIndicatorVisibility: Backport<Any>.ScrollIndicatorVisibility {
get { self[BackportHorizontalIndicatorKey.self] }
set { self[BackportHorizontalIndicatorKey.self] = newValue }
}
@available(macOS 10.15, *)
/// The way that scrollable content interacts with the software keyboard.
///
/// The default value is ``Backport.ScrollDismissesKeyboardMode.automatic``. Use the
/// ``View.backport.scrollDismissesKeyboard(_:)`` modifier to configure this
/// property.
public var backportScrollDismissesKeyboardMode: Backport<Any>.ScrollDismissesKeyboardMode {
get { self[BackportKeyboardDismissKey.self] }
set { self[BackportKeyboardDismissKey.self] = newValue }
}
@available(macOS 10.15, *)
/// A Boolean value that indicates whether any scroll views associated
/// with this environment allow scrolling to occur.
///
/// The default value is `true`. Use the ``View.backport.scrollDisabled(_:)``
/// modifier to configure this property.
public var backportIsScrollEnabled: Bool {
get { self[BackportScrollEnabledKey.self] }
set { self[BackportScrollEnabledKey.self] = newValue }
}
}
@available(macOS 10.15, *)
private struct BackportVerticalIndicatorKey: EnvironmentKey {
static var defaultValue: Backport<Any>.ScrollIndicatorVisibility = .automatic
}
@available(macOS 10.15, *)
private struct BackportHorizontalIndicatorKey: EnvironmentKey {
static var defaultValue: Backport<Any>.ScrollIndicatorVisibility = .automatic
}
@available(macOS 10.15, *)
private struct BackportKeyboardDismissKey: EnvironmentKey {
static var defaultValue: Backport<Any>.ScrollDismissesKeyboardMode = .automatic
}
@available(macOS 10.15, *)
private struct BackportScrollEnabledKey: EnvironmentKey {
static var defaultValue: Bool = true
}

View File

@ -1,63 +0,0 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// The ways that scrollable content can interact with the software keyboard.
///
/// Use this type in a call to the ``View.backport.scrollDismissesKeyboard(_:)``
/// modifier to specify the dismissal behavior of scrollable views.
public struct ScrollDismissesKeyboardMode: Hashable, CustomStringConvertible {
internal enum DismissMode: Hashable {
case automatic
case immediately
case interactively
case never
}
let dismissMode: DismissMode
#if os(iOS)
var scrollViewDismissMode: UIScrollView.KeyboardDismissMode {
switch dismissMode {
case .automatic: return .none
case .immediately: return .onDrag
case .interactively: return .interactive
case .never: return .none
}
}
#endif
public var description: String {
String(describing: dismissMode)
}
/// Determine the mode automatically based on the surrounding context.
///
/// By default, a ``TextEditor`` is interactive while a ``List``
/// of scrollable content always dismiss the keyboard on a scroll
public static var automatic: Self { .init(dismissMode: .automatic) }
/// Dismiss the keyboard as soon as scrolling starts.
public static var immediately: Self { .init(dismissMode: .immediately) }
/// Enable people to interactively dismiss the keyboard as part of the
/// scroll operation.
///
/// The software keyboard's position tracks the gesture that drives the
/// scroll operation if the gesture crosses into the keyboard's area of the
/// display. People can dismiss the keyboard by scrolling it off the
/// display, or reverse the direction of the scroll to cancel the dismissal.
public static var interactively: Self { .init(dismissMode: .interactively) }
/// Never dismiss the keyboard automatically as a result of scrolling.
public static var never: Self { .init(dismissMode: .never) }
}
}

Some files were not shown because too many files have changed in this diff Show More