├── .gitignore ├── .jazzy.yaml ├── .swiftformat ├── CHANGELOG.md ├── CHANGELOG_CN.md ├── Example ├── Build.sh ├── FWFramework.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── FWFramework-Example.xcscheme ├── FWFramework.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── FWFramework │ ├── Assets │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon.jpg │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── guideArrow.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── guideArrow@2x.png │ │ │ │ └── guideArrow@3x.png │ │ │ ├── qrcodeFlashlightClose.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── qrcodeFlashlightClose@2x.png │ │ │ ├── qrcodeFlashlightOpen.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── qrcodeFlashlightOpen@2x.png │ │ │ ├── qrcodeLine.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── qrcodeLine.png │ │ │ ├── themeColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── themeImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── themeImageDark@2x.png │ │ │ │ └── themeImageLight@2x.png │ │ │ ├── themeImageDark.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── themeImageDark@2x.png │ │ │ └── themeImageLight.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── themeImageLight@2x.png │ │ ├── LaunchScreen.storyboard │ │ ├── Resources │ │ │ ├── Animation.png │ │ │ ├── Audio1.mp3 │ │ │ ├── Audio2.m4a │ │ │ ├── Audio3.m4a │ │ │ ├── Bridge.html │ │ │ ├── Loading.gif │ │ │ ├── Lottie.json │ │ │ ├── Material.ttf │ │ │ ├── Qrcode.caf │ │ │ └── Video.mp4 │ │ ├── en.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ ├── zh-Hans.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ └── zh-Hant.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ ├── Classes │ │ ├── App │ │ │ ├── AppDelegate.swift │ │ │ ├── Config │ │ │ │ └── AppConfig.swift │ │ │ └── Router │ │ │ │ ├── AppMediator.swift │ │ │ │ └── AppRouter.swift │ │ ├── Library │ │ │ └── Standard │ │ │ │ ├── AppIcon.swift │ │ │ │ └── AppTheme.swift │ │ └── Module │ │ │ ├── Home │ │ │ ├── Index │ │ │ │ └── HomeController.swift │ │ │ ├── Tab │ │ │ │ └── TabController.swift │ │ │ └── Web │ │ │ │ └── WebController.swift │ │ │ ├── Settings │ │ │ └── Index │ │ │ │ └── SettingsController.swift │ │ │ ├── Test │ │ │ ├── Index │ │ │ │ └── TestController.swift │ │ │ ├── Kernel │ │ │ │ ├── TestConcurrencyController.swift │ │ │ │ ├── TestPromiseController.swift │ │ │ │ ├── TestRouterController.swift │ │ │ │ ├── TestStateController.swift │ │ │ │ └── TestWorkflowController.swift │ │ │ ├── Module │ │ │ │ ├── TestBadgeController.swift │ │ │ │ ├── TestBannerController.swift │ │ │ │ ├── TestBarrageController.swift │ │ │ │ ├── TestBridgeController.swift │ │ │ │ ├── TestDrawerController.swift │ │ │ │ ├── TestFloatingController.swift │ │ │ │ ├── TestGridController.swift │ │ │ │ ├── TestPagingController.swift │ │ │ │ ├── TestPasscodeController.swift │ │ │ │ ├── TestPopupController.swift │ │ │ │ ├── TestQrcodeController.swift │ │ │ │ ├── TestSegmentController.swift │ │ │ │ ├── TestSkeletonController.swift │ │ │ │ ├── TestStatisticalController.swift │ │ │ │ ├── TestSwiftController.swift │ │ │ │ ├── TestSwiftUIController.swift │ │ │ │ ├── TestSwiftUIListController.swift │ │ │ │ ├── TestTabbarController.swift │ │ │ │ ├── TestTabbarViewController.swift │ │ │ │ ├── TestToolbarController.swift │ │ │ │ └── TestTransitionController.swift │ │ │ ├── Plugin │ │ │ │ ├── TestAlertController.swift │ │ │ │ ├── TestCameraController.swift │ │ │ │ ├── TestEmptyController.swift │ │ │ │ ├── TestImageController.swift │ │ │ │ ├── TestPickerController.swift │ │ │ │ ├── TestPluginController.swift │ │ │ │ ├── TestPreviewController.swift │ │ │ │ ├── TestRefreshController.swift │ │ │ │ └── TestToastController.swift │ │ │ ├── Service │ │ │ │ ├── TestAudioController.swift │ │ │ │ ├── TestAuthorizeController.swift │ │ │ │ ├── TestCacheController.swift │ │ │ │ ├── TestCodableController.swift │ │ │ │ ├── TestDatabaseController.swift │ │ │ │ ├── TestEncodeController.swift │ │ │ │ ├── TestNotificationController.swift │ │ │ │ ├── TestRequestController.swift │ │ │ │ ├── TestSocketController.swift │ │ │ │ ├── TestThemeController.swift │ │ │ │ └── TestVideoController.swift │ │ │ └── Toolkit │ │ │ │ ├── TestAdaptiveController.swift │ │ │ │ ├── TestAnimationController.swift │ │ │ │ ├── TestButtonController.swift │ │ │ │ ├── TestCollectionController.swift │ │ │ │ ├── TestCompatibleController.swift │ │ │ │ ├── TestIconController.swift │ │ │ │ ├── TestKeyboardController.swift │ │ │ │ ├── TestLayoutController.swift │ │ │ │ ├── TestTableController.swift │ │ │ │ └── TestThreadController.swift │ │ │ └── User │ │ │ └── Login │ │ │ ├── Controller │ │ │ └── LoginController.swift │ │ │ ├── Model │ │ │ └── UserModel.swift │ │ │ ├── Service │ │ │ └── UserService.swift │ │ │ └── ViewModel │ │ │ └── LoginViewModel.swift │ └── Info.plist ├── FWPlugin │ ├── FWPlugin.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── FWPlugin.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── FWPlugin │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-1024.0@1x.jpg │ │ │ │ ├── Icon-20.0@1x.png │ │ │ │ ├── Icon-20.0@2x.png │ │ │ │ ├── Icon-20.0@3x.png │ │ │ │ ├── Icon-29.0@1x.png │ │ │ │ ├── Icon-29.0@2x.png │ │ │ │ ├── Icon-29.0@3x.png │ │ │ │ ├── Icon-40.0@1x.png │ │ │ │ ├── Icon-40.0@2x.png │ │ │ │ ├── Icon-40.0@3x.png │ │ │ │ ├── Icon-60.0@2x.png │ │ │ │ ├── Icon-60.0@3x.png │ │ │ │ ├── Icon-76.0@1x.png │ │ │ │ ├── Icon-76.0@2x.png │ │ │ │ └── Icon-83.5@2x.png │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift │ └── Podfile ├── FWSwiftUI │ ├── ExampleWidget │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── WidgetBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── address.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── address@2x.png │ │ │ ├── delivery.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── delivery@2x.png │ │ │ └── grocery.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── grocery@2x.png │ │ │ │ └── grocery@3x.png │ │ ├── ExampleWidget.entitlements │ │ ├── ExampleWidget.swift │ │ ├── ExampleWidgetBundle.swift │ │ ├── ExampleWidgetLiveActivity.swift │ │ └── Info.plist │ ├── FWSwiftUI.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── FWSwiftUI │ │ ├── ActivityManager.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-1024.0@1x.jpg │ │ │ ├── Icon-20.0@1x.png │ │ │ ├── Icon-20.0@2x.png │ │ │ ├── Icon-20.0@3x.png │ │ │ ├── Icon-29.0@1x.png │ │ │ ├── Icon-29.0@2x.png │ │ │ ├── Icon-29.0@3x.png │ │ │ ├── Icon-40.0@1x.png │ │ │ ├── Icon-40.0@2x.png │ │ │ ├── Icon-40.0@3x.png │ │ │ ├── Icon-60.0@2x.png │ │ │ ├── Icon-60.0@3x.png │ │ │ ├── Icon-76.0@1x.png │ │ │ ├── Icon-76.0@2x.png │ │ │ └── Icon-83.5@2x.png │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── ExampleApp.swift │ │ ├── ExampleWidgetAttributes.swift │ │ ├── FWSwiftUI.entitlements │ │ ├── Info.plist │ │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Podfile ├── fastlane │ ├── .env.default │ ├── Appfile │ ├── Fastfile │ ├── Pluginfile │ └── README.md └── scripts │ ├── build.sh │ ├── docc.sh │ ├── framework.sh │ ├── jazzy.sh │ ├── mmkv.sh │ ├── package_docc.sh │ ├── package_framework.sh │ ├── package_name.sh │ └── test.sh ├── FWFramework.podspec ├── LICENSE ├── Package.swift ├── README.md ├── README_CN.md └── Sources ├── FWFramework ├── Kernel │ ├── Autoloader.swift │ ├── Benchmark.swift │ ├── Bridge.swift │ ├── Configuration.swift │ ├── Error.swift │ ├── Loader.swift │ ├── Logger.swift │ ├── Mediator.swift │ ├── Message.swift │ ├── Navigator.swift │ ├── Parameter.swift │ ├── Plugin.swift │ ├── Promise.swift │ ├── Proxy.swift │ ├── Router.swift │ ├── Runtime.swift │ ├── State.swift │ ├── Stdlib.swift │ ├── Swizzle.swift │ ├── Task.swift │ ├── Test.swift │ ├── Thread.swift │ └── Wrapper.swift ├── Service │ ├── Cache │ │ ├── CacheEngine.swift │ │ ├── CacheFile.swift │ │ ├── CacheKeychain.swift │ │ ├── CacheManager.swift │ │ ├── CacheMemory.swift │ │ ├── CacheSqlite.swift │ │ └── CacheUserDefaults.swift │ ├── Cocoa │ │ ├── Authorize.swift │ │ ├── Location.swift │ │ └── Notification.swift │ ├── Media │ │ ├── AnimatedImage.swift │ │ ├── AssetManager.swift │ │ ├── AudioPlayer.swift │ │ └── VideoPlayer.swift │ ├── Model │ │ ├── AnyModel.swift │ │ ├── CodableModel.swift │ │ ├── JSONModel.swift │ │ └── SmartModel.swift │ ├── Network │ │ ├── HTTPSessionManager.swift │ │ ├── NetworkReachabilityManager.swift │ │ ├── SecurityPolicy.swift │ │ ├── URLRequestSerialization.swift │ │ ├── URLResponseSerialization.swift │ │ └── URLSessionManager.swift │ └── Request │ │ ├── BatchRequest.swift │ │ ├── ChainRequest.swift │ │ ├── HTTPRequest.swift │ │ ├── RequestConfig.swift │ │ ├── RequestManager.swift │ │ └── RequestPlugin.swift └── Toolkit │ ├── Adaptive.swift │ ├── Annotation.swift │ ├── Appearance.swift │ ├── Archiver.swift │ ├── AutoLayout.swift │ ├── BarAppearance.swift │ ├── BarStyle.swift │ ├── Block.swift │ ├── Bundle.swift │ ├── Chainable.swift │ ├── Codable.swift │ ├── Decoder.swift │ ├── DynamicLayout.swift │ ├── Encryptor.swift │ ├── Foundation.swift │ ├── Icon.swift │ ├── JSON.swift │ ├── Keyboard.swift │ ├── Keychain.swift │ ├── Language.swift │ ├── QuartzCore.swift │ ├── ResultBuilder.swift │ ├── Theme.swift │ ├── Toolkit.swift │ ├── UIKit.swift │ └── Validator.swift ├── FWPlugin ├── Alamofire │ └── AlamofireImpl.swift ├── Authorize │ ├── Biometry │ │ └── AuthorizeBiometry.swift │ ├── Calendar │ │ └── AuthorizeCalendar.swift │ ├── Contacts │ │ └── AuthorizeContacts.swift │ ├── Microphone │ │ └── AuthorizeMicrophone.swift │ └── Tracking │ │ └── AuthorizeTracking.swift ├── Lottie │ └── LottiePluginView.swift ├── MMKV │ └── CacheMMKV.swift ├── Macros │ ├── FWMacroMacros │ │ ├── FWMacroMacros.swift │ │ ├── MappedValueMacro.swift │ │ ├── PropertyWrapperMacro.swift │ │ └── SmartSubclassMacro.swift │ ├── FWPluginMacros │ │ └── FWPluginMacros.swift │ └── Package.swift └── SDWebImage │ └── SDWebImageImpl.swift ├── FWSwiftUI ├── Extend │ ├── PopupView.swift │ └── WaterfallGrid.swift ├── Module │ ├── Controller │ │ ├── HostingController.swift │ │ ├── HostingView.swift │ │ ├── NavigationView.swift │ │ └── StateView.swift │ └── View │ │ ├── AttributedText.swift │ │ ├── ImageView.swift │ │ ├── PluginView.swift │ │ ├── RefreshView.swift │ │ └── WebImageView.swift └── Plugin │ ├── ViewBuilder.swift │ ├── ViewConfigure.swift │ ├── ViewContext.swift │ ├── ViewIntrospect.swift │ ├── ViewPreference.swift │ ├── ViewStorage.swift │ ├── ViewToolkit.swift │ └── ViewWrapper.swift ├── FWUIKit ├── Extend │ ├── Service │ │ ├── Analyzer.swift │ │ ├── Database.swift │ │ ├── NetworkMocker.swift │ │ ├── PlayerCache.swift │ │ ├── Recognizer.swift │ │ ├── VersionManager.swift │ │ └── WebSocket.swift │ └── View │ │ ├── BarrageView.swift │ │ ├── GuideView.swift │ │ ├── PagingView.swift │ │ ├── PasscodeView.swift │ │ ├── PopupMenu.swift │ │ ├── SkeletonView.swift │ │ └── StatisticalView.swift ├── Module │ ├── App │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ ├── Controller │ │ ├── CollectionViewController.swift │ │ ├── NavigationController.swift │ │ ├── PopupViewController.swift │ │ ├── RequestViewController.swift │ │ ├── ScrollViewController.swift │ │ ├── TabBarController.swift │ │ ├── TableViewController.swift │ │ ├── ViewController.swift │ │ └── WebViewController.swift │ ├── View │ │ ├── AttributedLabel.swift │ │ ├── BadgeView.swift │ │ ├── BannerView.swift │ │ ├── CollectionLayout.swift │ │ ├── CollectionView.swift │ │ ├── CornerView.swift │ │ ├── DrawerView.swift │ │ ├── FloatingView.swift │ │ ├── GradientView.swift │ │ ├── GridView.swift │ │ ├── MarqueeLabel.swift │ │ ├── PageControl.swift │ │ ├── ReusableView.swift │ │ ├── ScanView.swift │ │ ├── SegmentedControl.swift │ │ ├── TableView.swift │ │ ├── TagCollectionView.swift │ │ ├── ToolbarView.swift │ │ ├── ViewDelegate.swift │ │ ├── ViewProtocol.swift │ │ ├── ViewTransition.swift │ │ └── WebView.swift │ └── ViewModel │ │ └── ViewModel.swift └── Plugin │ ├── Alert │ ├── AlertController.swift │ ├── AlertPlugin.swift │ └── AlertPluginImpl.swift │ ├── Empty │ ├── EmptyPlugin.swift │ ├── EmptyPluginImpl.swift │ └── EmptyPluginView.swift │ ├── Image │ ├── ImageDownloader.swift │ ├── ImagePlugin.swift │ └── ImagePluginImpl.swift │ ├── Picker │ ├── ImageCropController.swift │ ├── ImagePickerController.swift │ ├── ImagePickerPlugin.swift │ └── ImagePickerPluginImpl.swift │ ├── Preview │ ├── ImagePreviewController.swift │ ├── ImagePreviewPlugin.swift │ ├── ImagePreviewPluginImpl.swift │ └── ZoomImageView.swift │ ├── Refresh │ ├── RefreshPlugin.swift │ ├── RefreshPluginImpl.swift │ └── RefreshPluginView.swift │ ├── Toast │ ├── ToastPlugin.swift │ ├── ToastPluginImpl.swift │ └── ToastPluginView.swift │ └── View │ ├── IndicatorPluginView.swift │ ├── ProgressPluginView.swift │ ├── ViewPlugin.swift │ └── ViewPluginImpl.swift └── PrivacyInfo.xcprivacy /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | .build/ 7 | build/ 8 | DerivedData/ 9 | docs/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | .swiftpm/ 22 | Package.resolved 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | .DS_Store 29 | .idea 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | # CocoaPods 38 | # 39 | # We recommend against adding the Pods directory to your .gitignore. However 40 | # you should judge for yourself, the pros and cons are mentioned at: 41 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 42 | # 43 | Pods/ 44 | Podfile.lock 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 59 | 60 | fastlane/report.xml 61 | fastlane/Preview.html 62 | fastlane/screenshots 63 | fastlane/test_output 64 | 65 | # Code Injection 66 | # 67 | # After new code Injection tools there's a generated folder /iOSInjectionProject 68 | # https://github.com/johnno1962/injectionforxcode 69 | 70 | iOSInjectionProject/ 71 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | github_url: https://github.com/lszzy/FWFramework 2 | output: docs 3 | theme: apple 4 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --exclude Pods 4 | --symlinks ignore 5 | --swiftversion 5.9 6 | 7 | # format options 8 | 9 | --commas inline 10 | --comments indent 11 | --decimalgrouping 3,5 12 | --exponentcase lowercase 13 | --exponentgrouping disabled 14 | --extensionacl on-declarations 15 | --fractiongrouping disabled 16 | --ifdef no-indent 17 | --importgrouping testable-top 18 | --operatorfunc no-space 19 | --nospaceoperators ..<, ... 20 | --self init-only 21 | --someAny false 22 | --stripunusedargs closure-only 23 | --wraparguments preserve 24 | --wrapcollections preserve 25 | --wrapparameters preserve 26 | --anonymousforeach ignore 27 | 28 | # rules 29 | 30 | --disable andOperator 31 | --disable opaqueGenericParameters 32 | --disable wrapMultilineStatementBraces 33 | --disable blankLinesAroundMark 34 | --disable redundantBackticks 35 | --disable initCoderUnavailable 36 | --disable conditionalAssignment 37 | --disable preferKeyPath 38 | --disable sortTypealiases 39 | --disable typeSugar 40 | -------------------------------------------------------------------------------- /Example/FWFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FWFramework.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/FWFramework.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.jpg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/guideArrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "guideArrow@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "guideArrow@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/guideArrow.imageset/guideArrow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/guideArrow.imageset/guideArrow@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/guideArrow.imageset/guideArrow@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/guideArrow.imageset/guideArrow@3x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/qrcodeFlashlightClose.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "qrcodeFlashlightClose@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/qrcodeFlashlightClose.imageset/qrcodeFlashlightClose@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/qrcodeFlashlightClose.imageset/qrcodeFlashlightClose@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/qrcodeFlashlightOpen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "qrcodeFlashlightOpen@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/qrcodeFlashlightOpen.imageset/qrcodeFlashlightOpen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/qrcodeFlashlightOpen.imageset/qrcodeFlashlightOpen@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/qrcodeLine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "qrcodeLine.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/qrcodeLine.imageset/qrcodeLine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/qrcodeLine.imageset/qrcodeLine.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "themeImageLight@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "themeImageDark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImage.imageset/themeImageDark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/themeImage.imageset/themeImageDark@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImage.imageset/themeImageLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/themeImage.imageset/themeImageLight@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImageDark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "themeImageDark@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImageDark.imageset/themeImageDark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/themeImageDark.imageset/themeImageDark@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImageLight.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "themeImageLight@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Assets.xcassets/themeImageLight.imageset/themeImageLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Assets.xcassets/themeImageLight.imageset/themeImageLight@2x.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Animation.png -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Audio1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Audio1.mp3 -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Audio2.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Audio2.m4a -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Audio3.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Audio3.m4a -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Loading.gif -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Material.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Material.ttf -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Qrcode.caf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Qrcode.caf -------------------------------------------------------------------------------- /Example/FWFramework/Assets/Resources/Video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWFramework/Assets/Resources/Video.mp4 -------------------------------------------------------------------------------- /Example/FWFramework/Assets/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | FWFramework 4 | 5 | Created by wuyong on 2022/8/18. 6 | Copyright © 2022 CocoaPods. All rights reserved. 7 | */ 8 | 9 | CFBundleDisplayName = "FWFramework"; 10 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | FWFramework 4 | 5 | Created by wuyong on 2022/8/18. 6 | Copyright © 2022 CocoaPods. All rights reserved. 7 | */ 8 | 9 | "mediatorLogin" = "Login"; 10 | "mediatorLogout" = "Logout"; 11 | "mediatorPlaceholder" = "Please enter your nickname"; 12 | "mediatorNickname" = "dear"; 13 | "homeTitle" = "Home"; 14 | "testTitle" = "Test"; 15 | "settingTitle" = "Settings"; 16 | "languageTitle" = "Language"; 17 | "themeTitle" = "Theme"; 18 | "systemTitle" = "System"; 19 | "themeLight" = "Light"; 20 | "themeDark" = "Dark"; 21 | "themeGray" = "Gray"; 22 | "welcomeTitle" = "Do you know that I'm waiting for you?"; 23 | "backTitle" = "Welcome back, %@"; 24 | "logoutConfirm" = "Are you sure you want to log out?"; 25 | "envDevelopment" = "Development"; 26 | "envTesting" = "Testing"; 27 | "envStaging" = "Staging"; 28 | "envProduction" = "Production"; 29 | "pluginTitle" = "Plugin"; 30 | "pluginDetail" = "Customize"; 31 | "pluginDemo" = "Demo"; 32 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | FWFramework 4 | 5 | Created by wuyong on 2022/8/18. 6 | Copyright © 2022 CocoaPods. All rights reserved. 7 | */ 8 | 9 | CFBundleDisplayName = "FWFramework"; 10 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | FWFramework 4 | 5 | Created by wuyong on 2022/8/18. 6 | Copyright © 2022 CocoaPods. All rights reserved. 7 | */ 8 | 9 | "mediatorLogin" = "登录"; 10 | "mediatorLogout" = "退出登录"; 11 | "mediatorPlaceholder" = "请输入你的昵称"; 12 | "mediatorNickname" = "亲"; 13 | "homeTitle" = "首页"; 14 | "testTitle" = "测试"; 15 | "settingTitle" = "设置"; 16 | "languageTitle" = "多语言"; 17 | "themeTitle" = "主题"; 18 | "systemTitle" = "系统"; 19 | "themeLight" = "浅色"; 20 | "themeDark" = "深色"; 21 | "themeGray" = "灰色"; 22 | "welcomeTitle" = "你知道我在等你吗?"; 23 | "backTitle" = "欢迎回来,%@"; 24 | "logoutConfirm" = "确定要退出当前账号吗?"; 25 | "envDevelopment" = "开发"; 26 | "envTesting" = "测试"; 27 | "envStaging" = "模拟"; 28 | "envProduction" = "正式"; 29 | "pluginTitle" = "插件"; 30 | "pluginDetail" = "定制"; 31 | "pluginDemo" = "演示"; 32 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/zh-Hant.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | FWFramework 4 | 5 | Created by wuyong on 2022/8/18. 6 | Copyright © 2022 CocoaPods. All rights reserved. 7 | */ 8 | 9 | CFBundleDisplayName = "FWFramework"; 10 | -------------------------------------------------------------------------------- /Example/FWFramework/Assets/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | FWFramework 4 | 5 | Created by wuyong on 2022/8/18. 6 | Copyright © 2022 CocoaPods. All rights reserved. 7 | */ 8 | 9 | "mediatorLogin" = "登入"; 10 | "mediatorLogout" = "退出登入"; 11 | "mediatorPlaceholder" = "請輸入你的暱稱"; 12 | "mediatorNickname" = "親"; 13 | "homeTitle" = "首頁"; 14 | "testTitle" = "測試"; 15 | "settingTitle" = "設定"; 16 | "languageTitle" = "多語言"; 17 | "themeTitle" = "主題"; 18 | "systemTitle" = "系統"; 19 | "themeLight" = "淺色"; 20 | "themeDark" = "深色"; 21 | "themeGray" = "灰色"; 22 | "welcomeTitle" = "你知道我在等你嗎?"; 23 | "backTitle" = "歡迎回來,%@"; 24 | "logoutConfirm" = "確定要退出目前帳號嗎?"; 25 | "envDevelopment" = "開發"; 26 | "envTesting" = "測試"; 27 | "envStaging" = "模擬"; 28 | "envProduction" = "正式"; 29 | "pluginTitle" = "插件"; 30 | "pluginDetail" = "客製化"; 31 | "pluginDemo" = "示範"; 32 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/App/Config/AppConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppConfig.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/5/10. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | class AppConfig: Configuration { 12 | var appId = "" 13 | } 14 | 15 | extension AppConfig { 16 | class Network: @unchecked Sendable { 17 | static let shared = Network() 18 | 19 | var apiUrl = "" 20 | } 21 | 22 | var network: Network { Network.shared } 23 | } 24 | 25 | class AppConfigTemplate: ConfigurationTemplate { 26 | override func applyConfiguration() { 27 | AppConfig.shared.appId = "appId" 28 | AppConfig.shared.network.apiUrl = "apiUrl" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/App/Router/AppMediator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppMediator.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/8/24. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | #if DEBUG 11 | import FWDebug 12 | #endif 13 | 14 | protocol AppModuleProtocol: ModuleProtocol { 15 | nonisolated func moduleMethod() 16 | } 17 | 18 | @objc extension Autoloader { 19 | func loadApp_AppModule() { 20 | Mediator.registerService(AppModuleProtocol.self, module: AppModule.self) 21 | } 22 | } 23 | 24 | /* final */ class AppModule: NSObject, AppModuleProtocol { 25 | /* public static let shared = AppModule() */ 26 | 27 | func setup() { 28 | #if DEBUG 29 | FWDebugManager.sharedInstance().openUrl = { url in 30 | if let scheme = URL.app.url(string: url)?.scheme, scheme.count > 0 { 31 | DispatchQueue.app.mainAsync { 32 | Router.openURL(url) 33 | } 34 | return true 35 | } 36 | return false 37 | } 38 | #endif 39 | 40 | DispatchQueue.main.async { 41 | ThemeManager.shared.overrideWindow = true 42 | } 43 | } 44 | 45 | func moduleMethod() { 46 | APP.debug("AppModule.moduleMethod") 47 | } 48 | 49 | // MARK: - UIApplicationDelegate 50 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 51 | NotificationManager.shared.clearNotificationBadges() 52 | if let remoteNotification = launchOptions?[.remoteNotification] { 53 | NotificationManager.shared.handleRemoteNotification(remoteNotification) 54 | } 55 | 56 | NotificationManager.shared.registerNotificationHandler() 57 | NotificationManager.shared.requestAuthorize(nil) 58 | NotificationManager.shared.remoteNotificationHandler = { userInfo, notification in 59 | NotificationManager.shared.clearNotificationBadges() 60 | 61 | var title: String? 62 | if let response = notification as? UNNotificationResponse { 63 | title = response.notification.request.content.title 64 | } 65 | UIWindow.app.showMessage(text: "收到远程通知:\(title ?? "")\n\(APP.safeString(userInfo))") 66 | } 67 | NotificationManager.shared.localNotificationHandler = { userInfo, notification in 68 | NotificationManager.shared.clearNotificationBadges() 69 | 70 | var title: String? 71 | if let response = notification as? UNNotificationResponse { 72 | title = response.notification.request.content.title 73 | } 74 | UIWindow.app.showMessage(text: "收到本地通知:\(title ?? "")\n\(APP.safeString(userInfo))") 75 | } 76 | return true 77 | } 78 | 79 | func applicationWillEnterForeground(_ application: UIApplication) { 80 | NotificationManager.shared.clearNotificationBadges() 81 | } 82 | 83 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 84 | UIDevice.app.setDeviceTokenData(deviceToken) 85 | } 86 | 87 | func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { 88 | UIDevice.app.setDeviceTokenData(nil) 89 | } 90 | 91 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 92 | NotificationManager.shared.handleRemoteNotification(userInfo) 93 | completionHandler(.newData) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/App/Router/AppRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppRouter.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/5/10. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | @MainActor class AppRouter: NSObject { 12 | @objc static let homeUrl = "app://home" 13 | @objc static let testUrl = "app://test" 14 | @objc static let settingsUrl = "app://settings" 15 | 16 | @objc static let httpUrl = "http://*" 17 | @objc static let httpsUrl = "https://*" 18 | } 19 | 20 | extension AppRouter { 21 | @objc static func homeRouter(_ context: Router.Context) -> Any? { 22 | let viewController = HomeController() 23 | return viewController 24 | } 25 | 26 | @objc static func testRouter(_ context: Router.Context) -> Any? { 27 | let viewController = TestController() 28 | return viewController 29 | } 30 | 31 | @objc static func settingsRouter(_ context: Router.Context) -> Any? { 32 | let viewController = SettingsController() 33 | return viewController 34 | } 35 | 36 | @objc static func httpRouter(_ context: Router.Context) -> Any? { 37 | httpsRouter(context) 38 | } 39 | 40 | @objc static func httpsRouter(_ context: Router.Context) -> Any? { 41 | let viewController = WebController(requestUrl: context.url) 42 | return viewController 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/Home/Index/HomeController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeController.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 05/07/2022. 6 | // Copyright (c) 2022 wuyong. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | class HomeController: UIViewController { 12 | // MARK: - Accessor 13 | private lazy var mediatorButton: UIButton = { 14 | let button = UIButton(type: .system) 15 | button.addTarget(self, action: #selector(onMediator), for: .touchUpInside) 16 | return button 17 | }() 18 | 19 | // MARK: - Lifecycle 20 | override func viewWillAppear(_ animated: Bool) { 21 | super.viewWillAppear(animated) 22 | 23 | renderData() 24 | } 25 | } 26 | 27 | extension HomeController: ViewControllerProtocol { 28 | func setupNavbar() { 29 | #if RELEASE 30 | let envTitle = APP.localized("envProduction") 31 | #elseif STAGING 32 | let envTitle = APP.localized("envStaging") 33 | #elseif TESTING 34 | let envTitle = APP.localized("envTesting") 35 | #else 36 | let envTitle = APP.localized("envDevelopment") 37 | #endif 38 | navigationItem.title = "\(APP.localized("homeTitle")) - \(envTitle)" 39 | } 40 | 41 | func setupSubviews() { 42 | view.addSubview(mediatorButton) 43 | } 44 | 45 | func setupLayout() { 46 | mediatorButton.app.layoutChain 47 | .horizontal() 48 | .top(toSafeArea: 20) 49 | .height(30) 50 | } 51 | 52 | private func renderData() { 53 | setupNavbar() 54 | 55 | if UserService.shared.isLogin() { 56 | mediatorButton.setTitle(String(format: APP.localized("backTitle"), UserService.shared.getUserModel()?.userName ?? ""), for: .normal) 57 | } else { 58 | mediatorButton.setTitle(APP.localized("welcomeTitle"), for: .normal) 59 | } 60 | } 61 | } 62 | 63 | // MARK: - Action 64 | extension HomeController { 65 | @objc func onMediator() { 66 | if UserService.shared.isLogin() { 67 | UserService.shared.logout { [weak self] in 68 | self?.renderData() 69 | } 70 | } else { 71 | UserService.shared.login { [weak self] in 72 | self?.renderData() 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/Test/Module/TestFloatingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestFloatingController.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/9/21. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | class TestFloatingController: UIViewController, ViewControllerProtocol { 12 | private lazy var floatView: FloatingView = { 13 | let result = FloatingView() 14 | result.padding = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) 15 | result.itemMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 16 | result.minimumItemSize = CGSize(width: 69, height: 29) 17 | result.layer.borderWidth = 0.5 18 | result.layer.borderColor = AppTheme.textColor.cgColor 19 | return result 20 | }() 21 | 22 | func setupSubviews() { 23 | view.addSubview(floatView) 24 | floatView.app.layoutChain 25 | .left(24) 26 | .right(24) 27 | .top(toSafeArea: 36) 28 | 29 | let suggestions = ["东野圭吾\n多行文本", "三体", "爱", "红楼梦", "", "理智与情感\n多行文本", "读书热榜", "免费榜"] 30 | for i in 0.. UITableView.Style { 13 | .grouped 14 | } 15 | 16 | func setupNavbar() { 17 | app.setRightBarItem(UIBarButtonItem.SystemItem.refresh.rawValue) { [weak self] _ in 18 | self?.app.hideEmptyView() 19 | self?.tableView.reloadData() 20 | } 21 | } 22 | 23 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 24 | view.app.hasEmptyView ? 0 : 6 25 | } 26 | 27 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 28 | let cell = UITableViewCell.app.cell(tableView: tableView) 29 | let row = indexPath.row 30 | if row == 0 { 31 | cell.textLabel?.text = "显示提示语" 32 | } else if row == 1 { 33 | cell.textLabel?.text = "显示提示语和详情" 34 | } else if row == 2 { 35 | cell.textLabel?.text = "显示图片和提示语" 36 | } else if row == 3 { 37 | cell.textLabel?.text = "显示提示语及操作按钮" 38 | } else if row == 4 { 39 | cell.textLabel?.text = "显示加载视图" 40 | } else if row == 5 { 41 | cell.textLabel?.text = "显示所有视图" 42 | } 43 | return cell 44 | } 45 | 46 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 47 | tableView.deselectRow(at: indexPath, animated: true) 48 | let row = indexPath.row 49 | if row == 0 { 50 | app.showEmptyView(text: "联系人为空") 51 | } else if row == 1 { 52 | app.showEmptyView(text: "联系人为空", detail: "请到设置-隐私查看你的联系人权限设置") 53 | } else if row == 2 { 54 | app.showEmptyView(text: "暂无数据", detail: nil, image: UIImage.app.appIconImage()) 55 | } else if row == 3 { 56 | app.showEmptyView(text: "请求失败", detail: "请检查网络连接", image: nil, action: "重试") { [weak self] _ in 57 | self?.app.hideEmptyView() 58 | self?.tableView.reloadData() 59 | } 60 | } else if row == 4 { 61 | app.showEmptyLoading() 62 | } else if row == 5 { 63 | app.showEmptyView(text: NSAttributedString(string: "请求失败", attributes: [.font: UIFont.app.semiboldFont(ofSize: 15), .foregroundColor: UIColor.red]), detail: "请检查网络连接", image: UIImage.app.appIconImage(), loading: true, actions: ["取消", NSAttributedString(string: "重试", attributes: [.font: UIFont.app.semiboldFont(ofSize: 15), .foregroundColor: UIColor.red])]) { [weak self] index, _ in 64 | if index == 0 { 65 | self?.app.showEmptyView(text: "请求失败", detail: "请检查网络连接", image: UIImage.app.appIconImage(), loading: true, actions: nil, block: nil) 66 | } else { 67 | self?.app.hideEmptyView() 68 | self?.tableView.reloadData() 69 | } 70 | } 71 | } 72 | tableView.reloadData() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/Test/Service/TestEncodeController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestEncodeController.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/8/24. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | enum TestEncodeEnum: Int { 12 | case `default` = 1 13 | } 14 | 15 | struct TestEncodeOption: OptionSet { 16 | let rawValue: Int 17 | 18 | init(rawValue: Int) { 19 | self.rawValue = rawValue 20 | } 21 | 22 | static let `default`: TestEncodeOption = .init(rawValue: 1 << 0) 23 | } 24 | 25 | struct TestEncodeStruct { 26 | var id: Int = 1 27 | var name: String = "name" 28 | } 29 | 30 | class TestEncodeClass { 31 | var id: Int = 1 32 | var name: String = "name" 33 | } 34 | 35 | class TestEncodeObject: NSObject, @unchecked Sendable { 36 | var id: Int = 1 37 | var name: String = "name" 38 | var closure: () -> Void = {} 39 | 40 | @objc func testFunction() {} 41 | } 42 | 43 | class TestEncodeController: UIViewController, TableViewControllerProtocol { 44 | func setupTableStyle() -> UITableView.Style { 45 | .grouped 46 | } 47 | 48 | func setupSubviews() { 49 | let encodeList: [[Any]] = [ 50 | ["String", "1", "1"], 51 | ["Int", "1", 1], 52 | ["Double", "1.0", 1.0], 53 | ["NSString", "NSString(string: \"name\")", NSString(string: "name")], 54 | ["NSNumber", "NSNumber(value: 1)", NSNumber(value: 1)], 55 | ["NSNull", "NSNull()", NSNull()], 56 | ["Enum", "TestEncodeEnum.default", TestEncodeEnum.default], 57 | ["OptionSet", "TestEncodeOption.default", TestEncodeOption.default], 58 | ["Struct", "TestEncodeStruct()", TestEncodeStruct()], 59 | ["Class", "TestEncodeClass()", TestEncodeClass()], 60 | ["NSObject", "TestEncodeObject()", TestEncodeObject()], 61 | ["AnyClass", "TestEncodeObject.self", TestEncodeObject.self], 62 | ["Function", "TestEncodeObject.testFunction", TestEncodeObject.testFunction], 63 | ["Closure", "TestEncodeObject().closure", TestEncodeObject().closure], 64 | ["Selector", "#selector(TestEncodeObject.testFunction)", #selector(TestEncodeObject.testFunction)], 65 | ["Struct.Type", "TestEncodeStruct.self", TestEncodeStruct.self], 66 | ["Class.Type", "TestEncodeClass.self", TestEncodeClass.self], 67 | ["NSObject.Type", "TestEncodeObject.self", TestEncodeObject.self] 68 | ] 69 | tableData.append(contentsOf: encodeList) 70 | } 71 | 72 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 73 | tableData.count 74 | } 75 | 76 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 77 | let rowData = tableData[indexPath.row] as! [Any] 78 | var text = (rowData[0] as! String) + ": " + (rowData[1] as! String) 79 | text += "\nsafeString: \(APP.safeString(rowData[2]))" 80 | text += "\nsafeNumber: \(APP.safeNumber(rowData[2]))" 81 | 82 | let cell = UITableViewCell.app.cell(tableView: tableView) 83 | cell.textLabel?.numberOfLines = 0 84 | cell.textLabel?.text = text 85 | return cell 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/Test/Service/TestNotificationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestNotificationController.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/9/2. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | class TestNotificationController: UIViewController, TableViewControllerProtocol { 12 | typealias TableElement = [String] 13 | 14 | func setupTableStyle() -> UITableView.Style { 15 | .grouped 16 | } 17 | 18 | func setupTableView() { 19 | tableData.append(contentsOf: [ 20 | ["本地通知(不重复,立即)", "onNotification1"], 21 | ["本地通知(不重复,5秒后)", "onNotification2"], 22 | ["本地通知(重复,每1分钟)", "onNotification3"], 23 | ["取消本地通知(批量)", "onNotification4"], 24 | ["取消本地通知(所有)", "onNotification5"] 25 | ]) 26 | } 27 | 28 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 29 | tableData.count 30 | } 31 | 32 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | let cell = UITableViewCell.app.cell(tableView: tableView) 34 | let rowData = tableData[indexPath.row] 35 | cell.textLabel?.text = rowData[0] 36 | return cell 37 | } 38 | 39 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 40 | tableView.deselectRow(at: indexPath, animated: true) 41 | let rowData = tableData[indexPath.row] 42 | let selector = NSSelectorFromString(rowData[1]) 43 | _ = perform(selector) 44 | } 45 | } 46 | 47 | @objc extension TestNotificationController { 48 | func onNotification1() { 49 | NotificationManager.shared.registerLocalNotification("test", title: "立即通知", subtitle: nil, body: "body", userInfo: ["id": "test"], badge: 0, sound: "default", timeInterval: 0, repeats: false) { content in 50 | // iOS15时效性通知,需entitlements开启配置生效 51 | if #available(iOS 15.0, *) { 52 | content.interruptionLevel = .timeSensitive 53 | } 54 | } 55 | } 56 | 57 | func onNotification2() { 58 | NotificationManager.shared.registerLocalNotification("test2", title: "5秒后通知", subtitle: "subtitle", body: "body", userInfo: ["id": "test2"], badge: 1, sound: "default", timeInterval: 5, repeats: false) { content in 59 | // iOS15时效性通知,需entitlements开启配置生效 60 | if #available(iOS 15.0, *) { 61 | content.interruptionLevel = .timeSensitive 62 | } 63 | } 64 | } 65 | 66 | func onNotification3() { 67 | NotificationManager.shared.registerLocalNotification("test3", title: "重复1分钟通知", subtitle: "subtitle", body: "body", userInfo: ["id": "test3"], badge: 1, sound: "default", timeInterval: 60, repeats: true) 68 | } 69 | 70 | func onNotification4() { 71 | NotificationManager.shared.removeLocalNotification(["test", "test2", "test3"]) 72 | } 73 | 74 | func onNotification5() { 75 | NotificationManager.shared.removeAllLocalNotifications() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/User/Login/Controller/LoginController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginController.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/8/24. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | class LoginController: UIViewController { 12 | // MARK: - Accessor 13 | var completion: (() -> Void)? 14 | 15 | private var viewModel = LoginViewModel() 16 | 17 | // MARK: - Subviews 18 | private lazy var nicknameField: UITextField = { 19 | let result = UITextField() 20 | result.app.addStyle(.default) 21 | result.placeholder = "mediatorPlaceholder".app.localized 22 | return result 23 | }() 24 | 25 | private lazy var loginButton: UIButton = { 26 | let button = AppTheme.largeButton() 27 | button.setTitle(APP.localized("mediatorLogin"), for: .normal) 28 | button.app.addTouch(target: self, action: #selector(loginButtonClicked)) 29 | return button 30 | }() 31 | } 32 | 33 | // MARK: - ViewControllerProtocol 34 | extension LoginController: ViewControllerProtocol { 35 | func setupNavbar() { 36 | navigationItem.title = APP.localized("mediatorLogin") 37 | app.setLeftBarItem(Icon.closeImage) { [weak self] _ in 38 | self?.app.close(animated: true) 39 | } 40 | } 41 | 42 | func setupSubviews() { 43 | view.addSubview(nicknameField) 44 | view.addSubview(loginButton) 45 | } 46 | 47 | func setupLayout() { 48 | nicknameField.layoutChain 49 | .horizontal(15) 50 | .height(50) 51 | .top(toSafeArea: 20) 52 | 53 | loginButton.layoutChain 54 | .top(toViewBottom: nicknameField, offset: 20) 55 | .centerX() 56 | } 57 | } 58 | 59 | // MARK: - Action 60 | extension LoginController { 61 | @objc private func loginButtonClicked() { 62 | let nickname = nicknameField.text?.app.trimString ?? "" 63 | viewModel.login(nickName: nickname, completion: { [weak self] in 64 | self?.dismiss(animated: true, completion: self?.completion) 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/User/Login/Model/UserModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserModel.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2024/5/16. 6 | // Copyright © 2024 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | public struct UserModel: Codable, AnyArchivable { 12 | public var userId: String = "" 13 | public var userName: String = "" 14 | 15 | public init() {} 16 | } 17 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/User/Login/Service/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserService.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2022/5/10. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import FWFramework 10 | 11 | public class UserService: @unchecked Sendable { 12 | public static let shared = UserService() 13 | 14 | // MARK: - Notification 15 | public static let loginNotification = Notification.Name("loginNotification") 16 | public static let logoutNotification = Notification.Name("logoutNotification") 17 | 18 | // MARK: - Accessor 19 | @StoredValue("userModel") 20 | private var userModel: UserModel? 21 | 22 | // MARK: - Lifecycle 23 | private init() {} 24 | } 25 | 26 | extension UserService { 27 | public func isLogin() -> Bool { 28 | userModel != nil 29 | } 30 | 31 | public func getUserModel() -> UserModel? { 32 | guard var model = userModel else { return nil } 33 | if model.userName.isEmpty { 34 | model.userName = APP.localized("mediatorNickname") 35 | } 36 | return model 37 | } 38 | 39 | public func saveUserModel(_ model: UserModel) { 40 | userModel = model 41 | NSObject.app.postNotification(UserService.loginNotification) 42 | } 43 | 44 | public func clearUserModel() { 45 | userModel = nil 46 | NSObject.app.postNotification(UserService.logoutNotification) 47 | } 48 | 49 | @MainActor public func login(_ completion: (() -> Void)? = nil) { 50 | let viewController = LoginController() 51 | viewController.completion = completion 52 | let navigationController = UINavigationController(rootViewController: viewController) 53 | Navigator.present(navigationController, animated: true, completion: nil) 54 | } 55 | 56 | @MainActor public func logout(_ completion: (() -> Void)? = nil) { 57 | let viewController = Navigator.topViewController 58 | viewController?.app.showConfirm(title: APP.localized("logoutConfirm"), message: nil) { [weak self] in 59 | self?.clearUserModel() 60 | completion?() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Example/FWFramework/Classes/Module/User/Login/ViewModel/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // FWFramework_Example 4 | // 5 | // Created by wuyong on 2024/5/16. 6 | // Copyright © 2024 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LoginViewModel { 12 | func login(nickName: String, completion: @escaping () -> Void) { 13 | var userModel = UserModel() 14 | userModel.userId = "1" 15 | userModel.userName = nickName 16 | 17 | UserService.shared.saveUserModel(userModel) 18 | completion() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/FWFramework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLName 27 | app 28 | CFBundleURLSchemes 29 | 30 | app 31 | 32 | 33 | 34 | CFBundleVersion 35 | $(CURRENT_PROJECT_VERSION) 36 | LSRequiresIPhoneOS 37 | 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | 43 | NSAppleMusicUsageDescription 44 | 请点击“好”以允许访问。 45 | NSCalendarsFullAccessUsageDescription 46 | 请点击“好”以允许访问。 47 | NSCalendarsUsageDescription 48 | 请点击“好”以允许访问。 49 | NSCalendarsWriteOnlyAccessUsageDescription 50 | 请点击“好”以允许访问。 51 | NSCameraUsageDescription 52 | 请点击“好”以允许访问。 53 | NSContactsUsageDescription 54 | 请点击“好”以允许访问。 55 | NSLocationAlwaysAndWhenInUseUsageDescription 56 | 请点击“允许”以允许访问。 57 | NSLocationAlwaysUsageDescription 58 | 请点击“允许”以允许访问。 59 | NSLocationWhenInUseUsageDescription 60 | 请点击“允许”以允许访问。 61 | NSMicrophoneUsageDescription 62 | 请点击“好”以允许访问。 63 | NSPhotoLibraryAddUsageDescription 64 | 请点击“好”以允许访问。 65 | NSPhotoLibraryUsageDescription 66 | 请点击“好”以允许访问。 67 | NSRemindersFullAccessUsageDescription 68 | 请点击“好”以允许访问。 69 | NSRemindersUsageDescription 70 | 请点击“好”以允许访问。 71 | NSUserTrackingUsageDescription 72 | 请点击“好”以允许访问。 73 | NSFaceIDUsageDescription 74 | 请点击“允许”以允许访问。 75 | UIBackgroundModes 76 | 77 | remote-notification 78 | 79 | UILaunchStoryboardName 80 | LaunchScreen 81 | UIRequiredDeviceCapabilities 82 | 83 | armv7 84 | 85 | UIRequiresFullScreen 86 | 87 | UISupportedInterfaceOrientations 88 | 89 | UIInterfaceOrientationLandscapeLeft 90 | UIInterfaceOrientationLandscapeRight 91 | UIInterfaceOrientationPortrait 92 | UIInterfaceOrientationPortraitUpsideDown 93 | 94 | UIViewControllerBasedStatusBarAppearance 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FWPlugin 4 | // 5 | // Created by wuyong on 2024/4/30. 6 | // 7 | 8 | import FWFramework 9 | import UIKit 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 14 | Autoloader.autoload() 15 | Mediator.setupAllModules() 16 | return true 17 | } 18 | 19 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 20 | // Override point for customization after application launch. 21 | true 22 | } 23 | 24 | // MARK: UISceneSession Lifecycle 25 | 26 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 27 | // Called when a new scene session is being created. 28 | // Use this method to select a configuration to create the new scene with. 29 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 30 | } 31 | 32 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 33 | // Called when the user discards a scene session. 34 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 35 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-20.0@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-20.0@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-29.0@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-29.0@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-40.0@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-40.0@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-60.0@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60.0@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-20.0@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-20.0@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-29.0@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-29.0@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-40.0@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-40.0@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-76.0@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-76.0@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon-1024.0@1x.jpg", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-1024.0@1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-1024.0@1x.jpg -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-20.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-20.0@1x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-20.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-20.0@2x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-20.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-20.0@3x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-29.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-29.0@1x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-29.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-29.0@2x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-29.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-29.0@3x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-40.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-40.0@1x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-40.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-40.0@2x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-40.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-40.0@3x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-60.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-60.0@2x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-60.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-60.0@3x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-76.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-76.0@1x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-76.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-76.0@2x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWPlugin/FWPlugin/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // FWPlugin 4 | // 5 | // Created by wuyong on 2024/4/30. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | var window: UIWindow? 12 | 13 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 14 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 15 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 16 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 17 | guard let _ = (scene as? UIWindowScene) else { return } 18 | } 19 | 20 | func sceneDidDisconnect(_ scene: UIScene) { 21 | // Called as the scene is being released by the system. 22 | // This occurs shortly after the scene enters the background, or when its session is discarded. 23 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 24 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 25 | } 26 | 27 | func sceneDidBecomeActive(_ scene: UIScene) { 28 | // Called when the scene has moved from an inactive state to an active state. 29 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 30 | } 31 | 32 | func sceneWillResignActive(_ scene: UIScene) { 33 | // Called when the scene will move from an active state to an inactive state. 34 | // This may occur due to temporary interruptions (ex. an incoming phone call). 35 | } 36 | 37 | func sceneWillEnterForeground(_ scene: UIScene) { 38 | // Called as the scene transitions from the background to the foreground. 39 | // Use this method to undo the changes made on entering the background. 40 | } 41 | 42 | func sceneDidEnterBackground(_ scene: UIScene) { 43 | // Called as the scene transitions from the foreground to the background. 44 | // Use this method to save data, release shared resources, and store enough scene-specific state information 45 | // to restore the scene back to its current state. 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/FWPlugin/FWPlugin/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // FWPlugin 4 | // 5 | // Created by wuyong on 2024/4/30. 6 | // 7 | 8 | import FWFramework 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | navigationItem.title = "Example" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/FWPlugin/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '13.0' 3 | use_frameworks! 4 | 5 | target "FWFramework_Plugin" do 6 | pod 'FWFramework/FWFramework/Kernel', :path => '../../' 7 | pod 'FWFramework/FWFramework/Toolkit', :path => '../../' 8 | pod 'FWFramework/FWFramework/Service', :path => '../../' 9 | pod 'FWFramework/FWUIKit/Plugin', :path => '../../' 10 | pod 'FWFramework/FWUIKit/Module', :path => '../../' 11 | pod 'FWFramework/FWUIKit/Extend', :path => '../../' 12 | pod 'FWFramework/FWSwiftUI/Plugin', :path => '../../' 13 | pod 'FWFramework/FWSwiftUI/Module', :path => '../../' 14 | pod 'FWFramework/FWSwiftUI/Extend', :path => '../../' 15 | pod 'FWFramework/FWPlugin/Contacts', :path => '../../' 16 | pod 'FWFramework/FWPlugin/Microphone', :path => '../../' 17 | pod 'FWFramework/FWPlugin/Calendar', :path => '../../' 18 | pod 'FWFramework/FWPlugin/Tracking', :path => '../../' 19 | pod 'FWFramework/FWPlugin/Biometry', :path => '../../' 20 | pod 'FWFramework/FWPlugin/SDWebImage', :path => '../../' 21 | pod 'FWFramework/FWPlugin/Alamofire', :path => '../../' 22 | pod 'FWFramework/FWPlugin/Lottie', :path => '../../' 23 | pod 'FWFramework/FWPlugin/MMKV', :path => '../../' 24 | pod 'FWFramework/FWPlugin/Macros', :path => '../../' 25 | 26 | pod 'FWDebug', :configurations => ['Debug'] 27 | end 28 | 29 | post_install do |installer| 30 | installer.pods_project.targets.each do |target| 31 | if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle" 32 | target.build_configurations.each do |config| 33 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 34 | end 35 | end 36 | end 37 | 38 | installer.generated_projects.each do |project| 39 | project.targets.each do |target| 40 | target.build_configurations.each do |config| 41 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 42 | end 43 | end 44 | end 45 | 46 | installer.pods_project.targets.each do |target| 47 | target.build_configurations.each do |config| 48 | config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO' 49 | config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -Xfrontend -load-plugin-executable -Xfrontend ${PODS_BUILD_DIR}/Products/FWMacroMacros/release/FWMacroMacros#FWMacroMacros' 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/address.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "address@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/address.imageset/address@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/ExampleWidget/Assets.xcassets/address.imageset/address@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/delivery.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "delivery@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/delivery.imageset/delivery@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/ExampleWidget/Assets.xcassets/delivery.imageset/delivery@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/grocery.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "grocery@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "grocery@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/grocery.imageset/grocery@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/ExampleWidget/Assets.xcassets/grocery.imageset/grocery@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Assets.xcassets/grocery.imageset/grocery@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/ExampleWidget/Assets.xcassets/grocery.imageset/grocery@3x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/ExampleWidget.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/ExampleWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleWidget.swift 3 | // ExampleWidget 4 | // 5 | // Created by wuyong on 2024/9/4. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | 11 | struct Provider: TimelineProvider { 12 | func placeholder(in context: Context) -> SimpleEntry { 13 | SimpleEntry(date: Date(), emoji: "😀") 14 | } 15 | 16 | func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) { 17 | let entry = SimpleEntry(date: Date(), emoji: "😀") 18 | completion(entry) 19 | } 20 | 21 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { 22 | var entries: [SimpleEntry] = [] 23 | 24 | let currentDate = Date() 25 | for hourOffset in 0..<5 { 26 | let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! 27 | let entry = SimpleEntry(date: entryDate, emoji: "😀") 28 | entries.append(entry) 29 | } 30 | 31 | let timeline = Timeline(entries: entries, policy: .atEnd) 32 | completion(timeline) 33 | } 34 | } 35 | 36 | struct SimpleEntry: TimelineEntry { 37 | let date: Date 38 | let emoji: String 39 | } 40 | 41 | struct ExampleWidgetEntryView: View { 42 | var entry: Provider.Entry 43 | 44 | var body: some View { 45 | VStack { 46 | Text("Time:") 47 | Text(entry.date, style: .time) 48 | 49 | Text("Emoji:") 50 | Text(entry.emoji) 51 | } 52 | .widgetURL(URL(string: "widget://widget")) 53 | } 54 | } 55 | 56 | struct ExampleWidget: Widget { 57 | let kind: String = "ExampleWidget" 58 | 59 | var body: some WidgetConfiguration { 60 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 61 | if #available(iOS 17.0, *) { 62 | ExampleWidgetEntryView(entry: entry) 63 | .containerBackground(.fill.tertiary, for: .widget) 64 | } else { 65 | ExampleWidgetEntryView(entry: entry) 66 | .padding() 67 | .background() 68 | } 69 | } 70 | .configurationDisplayName("My Widget") 71 | .description("This is an example widget.") 72 | } 73 | } 74 | 75 | @available(iOS 17.0, *) 76 | #Preview(as: .systemSmall) { 77 | ExampleWidget() 78 | } timeline: { 79 | SimpleEntry(date: .now, emoji: "😀") 80 | SimpleEntry(date: .now, emoji: "🤩") 81 | } 82 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/ExampleWidgetBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleWidgetBundle.swift 3 | // ExampleWidget 4 | // 5 | // Created by wuyong on 2024/9/4. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | 11 | @main 12 | struct ExampleWidgetBundle: WidgetBundle { 13 | var body: some Widget { 14 | ExampleWidget() 15 | ExampleWidgetLiveActivity() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/ExampleWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/ActivityManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityManager.swift 3 | // FWFramework_SwiftUI 4 | // 5 | // Created by wuyong on 2024/9/5. 6 | // 7 | 8 | import ActivityKit 9 | import SwiftUI 10 | 11 | @available(iOS 16.1, *) 12 | struct ActivityManager { 13 | static func requestAuthorization( 14 | options: UNAuthorizationOptions = [.sound, .alert, .badge], 15 | completionHandler: ((Bool, Error?) -> Void)? = nil 16 | ) { 17 | UIApplication.shared.registerForRemoteNotifications() 18 | 19 | UNUserNotificationCenter.current().requestAuthorization(options: options, completionHandler: { success, error in 20 | completionHandler?(success, error) 21 | }) 22 | } 23 | 24 | @discardableResult 25 | static func createActivity(attributes: T, contentState: T.ContentState, tokenHandler: ((String) -> Void)? = nil) throws -> Activity { 26 | let activity = try Activity.request(attributes: attributes, contentState: contentState, pushType: .token) 27 | Task { 28 | for await tokenData in activity.pushTokenUpdates { 29 | let token = tokenData.map { String(format: "%02x", $0) }.joined() 30 | tokenHandler?(token) 31 | } 32 | } 33 | return activity 34 | } 35 | 36 | static func updateActivity(_ activity: Activity, using contentState: T.ContentState) { 37 | Task { 38 | await activity.update(using: contentState) 39 | } 40 | } 41 | 42 | static func endActivity(_ activity: Activity) { 43 | Task { 44 | await activity.end(dismissalPolicy: .immediate) 45 | } 46 | } 47 | 48 | static func endAllActivities(of type: T.Type) { 49 | Task { 50 | for activity in Activity.activities { 51 | await activity.end(dismissalPolicy: .immediate) 52 | } 53 | } 54 | } 55 | 56 | static func getAllActivities(of type: T.Type) -> [Activity] { 57 | Activity.activities.sorted { $0.id > $1.id } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-20.0@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-20.0@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-29.0@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-29.0@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-40.0@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-40.0@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-60.0@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60.0@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-20.0@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-20.0@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-29.0@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-29.0@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-40.0@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-40.0@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-76.0@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-76.0@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon-1024.0@1x.jpg", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-1024.0@1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-1024.0@1x.jpg -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-20.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-20.0@1x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-20.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-20.0@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-20.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-20.0@3x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-29.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-29.0@1x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-29.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-29.0@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-29.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-29.0@3x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-40.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-40.0@1x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-40.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-40.0@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-40.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-40.0@3x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-60.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-60.0@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-60.0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-60.0@3x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-76.0@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-76.0@1x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-76.0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-76.0@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lszzy/FWFramework/e990792901b5040eac5352984f16403cc1cfdecc/Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleApp.swift 3 | // FWSwiftUI 4 | // 5 | // Created by wuyong on 2024/4/26. 6 | // 7 | 8 | import FWFramework 9 | import FWUIKit 10 | import SwiftUI 11 | 12 | @main 13 | struct ExampleApp: App { 14 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 15 | 16 | @Environment(\.scenePhase) var scenePhase 17 | 18 | init() { 19 | print("App is starting") 20 | 21 | if #available(iOS 16.1, *) { 22 | ActivityManager.requestAuthorization() 23 | } 24 | } 25 | 26 | var body: some Scene { 27 | WindowGroup { 28 | Group { 29 | if #available(iOS 16.1, *) { 30 | ContentView() 31 | } else { 32 | VStack { 33 | Image(systemName: "globe") 34 | .imageScale(.large) 35 | 36 | Text("Hello, world!") 37 | } 38 | .padding() 39 | } 40 | } 41 | .onOpenURL { url in 42 | print("Received URL: \(url)") 43 | UIWindow.wrapperExtension.showMessage(text: url.absoluteString) 44 | } 45 | .onAppear { 46 | print("View onAppear") 47 | } 48 | .onDisappear { 49 | print("View onDisappear") 50 | } 51 | } 52 | .onChange(of: scenePhase) { phase in 53 | switch phase { 54 | case .active: 55 | print("App is active") 56 | case .inactive: 57 | print("App is inactive") 58 | case .background: 59 | print("App is in background") 60 | @unknown default: 61 | print("App is \(phase)") 62 | } 63 | } 64 | } 65 | } 66 | 67 | class AppDelegate: AppResponder {} 68 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/ExampleWidgetAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleWidgetAttributes.swift 3 | // FWSwiftUI 4 | // 5 | // Created by wuyong on 2024/9/5. 6 | // 7 | 8 | import ActivityKit 9 | import SwiftUI 10 | 11 | struct ExampleWidgetAttributes: ActivityAttributes, Identifiable { 12 | public struct ContentState: Codable, Hashable { 13 | var courierName: String 14 | var deliveryTime: Date 15 | } 16 | 17 | var id = UUID() 18 | var numberOfGroceyItems: Int 19 | } 20 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/FWSwiftUI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSSupportsLiveActivities 6 | 7 | CFBundleURLTypes 8 | 9 | 10 | CFBundleTypeRole 11 | Editor 12 | CFBundleURLName 13 | widget 14 | CFBundleURLSchemes 15 | 16 | widget 17 | 18 | 19 | 20 | UIBackgroundModes 21 | 22 | remote-notification 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/FWSwiftUI/FWSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '13.0' 3 | use_frameworks! 4 | 5 | target "FWFramework_Example" do 6 | project 'FWFramework', 'Staging' => :release, 'Testing' => :release 7 | 8 | pod 'FWFramework', :path => '../', :subspecs => ['FWFramework', 'FWUIKit', 'FWSwiftUI', 'FWPlugin'] 9 | pod 'FWDebug', :configurations => ['Debug', 'Testing'] 10 | end 11 | 12 | post_install do |installer| 13 | installer.pods_project.targets.each do |target| 14 | target.build_configurations.each do |config| 15 | # 处理bundle资源编译打包问题 16 | if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle" 17 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 18 | end 19 | 20 | # 处理target编译最低版本问题 21 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 22 | 23 | # 处理FWPlugin/Macros跨模块调用问题 24 | config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO' 25 | config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -Xfrontend -load-plugin-executable -Xfrontend ${PODS_BUILD_DIR}/Products/FWMacroMacros/release/FWMacroMacros#FWMacroMacros' 26 | 27 | # 选择性开启框架Swift6编译模式 28 | # if ['FWFramework'].include? target.name 29 | # config.build_settings['SWIFT_VERSION'] = '6' 30 | # end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /Example/fastlane/.env.default: -------------------------------------------------------------------------------- 1 | FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="{PASSWORD}" 2 | -------------------------------------------------------------------------------- /Example/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | # https://docs.fastlane.tools/advanced/#appfile 2 | app_identifier("{app_identifier}") # The bundle identifier of your app 3 | apple_id("{apple_id}") # Your Apple email address 4 | # https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/user/detail 5 | itc_team_id("{itc_team_id}") # App Store Connect Team ID 6 | team_id("{team_id}") # Developer Portal Team ID 7 | 8 | # https://docs.fastlane.tools/advanced/Appfile/ 9 | for_lane :testflight_testing do 10 | app_identifier("{app_identifier}") 11 | apple_id("{apple_id}") 12 | itc_team_id("{itc_team_id}") 13 | team_id("{team_id}") 14 | end 15 | -------------------------------------------------------------------------------- /Example/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'cocoapods' 6 | gem 'fastlane-plugin-pgyer' 7 | -------------------------------------------------------------------------------- /Example/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios pgyer_testing 20 | ``` 21 | fastlane ios pgyer_testing 22 | ``` 23 | Push a new testing build to Pgyer 24 | ### ios pgyer_production 25 | ``` 26 | fastlane ios pgyer_production 27 | ``` 28 | Push a new production build to Pgyer 29 | ### ios testflight_testing 30 | ``` 31 | fastlane ios testflight_testing 32 | ``` 33 | Push a new testing build to TestFlight 34 | ### ios testflight_production 35 | ``` 36 | fastlane ios testflight_production 37 | ``` 38 | Push a new production build to TestFlight 39 | ### ios appstore_production 40 | ``` 41 | fastlane ios appstore_production 42 | ``` 43 | Push a new production build to the App Store 44 | 45 | ---- 46 | 47 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 48 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 49 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 50 | -------------------------------------------------------------------------------- /Example/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds a for all provided . 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | 8 | # Usage: 9 | # build.sh [ default:iOS macOS tvOS watchOS xrOS] 10 | # e.g. `bash scripts/build.sh MyTarget iOS macOS` 11 | 12 | # Exit immediately if a command exits with a non-zero status 13 | set -e 14 | 15 | # Verify that all required arguments are provided 16 | if [ $# -eq 0 ]; then 17 | echo "Error: This script requires at least one argument" 18 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 19 | echo "For instance: $0 MyTarget iOS macOS" 20 | exit 1 21 | fi 22 | 23 | # Define argument variables 24 | TARGET=$1 25 | 26 | # Remove TARGET from arguments list 27 | shift 28 | 29 | # Define platforms variable 30 | if [ $# -eq 0 ]; then 31 | set -- iOS macOS tvOS watchOS xrOS 32 | fi 33 | PLATFORMS=$@ 34 | 35 | # A function that builds $TARGET for a specific platform 36 | build_platform() { 37 | 38 | # Define a local $PLATFORM variable 39 | local PLATFORM=$1 40 | 41 | # Build $TARGET for the $PLATFORM 42 | echo "Building $TARGET for $PLATFORM..." 43 | if ! xcodebuild -scheme $TARGET -derivedDataPath .build -destination generic/platform=$PLATFORM; then 44 | echo "Failed to build $TARGET for $PLATFORM" 45 | return 1 46 | fi 47 | 48 | # Complete successfully 49 | echo "Successfully built $TARGET for $PLATFORM" 50 | } 51 | 52 | # Start script 53 | echo "" 54 | echo "Building $TARGET for [$PLATFORMS]..." 55 | echo "" 56 | 57 | # Loop through all platforms and call the build function 58 | for PLATFORM in $PLATFORMS; do 59 | if ! build_platform "$PLATFORM"; then 60 | exit 1 61 | fi 62 | done 63 | 64 | # Complete successfully 65 | echo "" 66 | echo "Building $TARGET completed successfully!" 67 | echo "" 68 | -------------------------------------------------------------------------------- /Example/scripts/docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds DocC for a and certain . 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | # The documentation ends up in to .build/docs-. 8 | 9 | # Usage: 10 | # docc.sh [ default:iOS macOS tvOS watchOS xrOS] 11 | # e.g. `bash scripts/docc.sh MyTarget iOS macOS` 12 | 13 | # Exit immediately if a command exits with a non-zero status 14 | set -e 15 | 16 | # Fail if any command in a pipeline fails 17 | set -o pipefail 18 | 19 | # Verify that all required arguments are provided 20 | if [ $# -eq 0 ]; then 21 | echo "Error: This script requires at least one argument" 22 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 23 | echo "For instance: $0 MyTarget iOS macOS" 24 | exit 1 25 | fi 26 | 27 | # Define argument variables 28 | TARGET=$1 29 | TARGET_LOWERCASED=$(echo "$1" | tr '[:upper:]' '[:lower:]') 30 | 31 | # Remove TARGET from arguments list 32 | shift 33 | 34 | # Define platforms variable 35 | if [ $# -eq 0 ]; then 36 | set -- iOS macOS tvOS watchOS xrOS 37 | fi 38 | PLATFORMS=$@ 39 | 40 | # Prepare the package for DocC 41 | swift package resolve; 42 | 43 | # A function that builds $TARGET for a specific platform 44 | build_platform() { 45 | 46 | # Define a local $PLATFORM variable and set an exit code 47 | local PLATFORM=$1 48 | local EXIT_CODE=0 49 | 50 | # Define the build folder name, based on the $PLATFORM 51 | case $PLATFORM in 52 | "iOS") 53 | DEBUG_PATH="Debug-iphoneos" 54 | ;; 55 | "macOS") 56 | DEBUG_PATH="Debug" 57 | ;; 58 | "tvOS") 59 | DEBUG_PATH="Debug-appletvos" 60 | ;; 61 | "watchOS") 62 | DEBUG_PATH="Debug-watchos" 63 | ;; 64 | "xrOS") 65 | DEBUG_PATH="Debug-xros" 66 | ;; 67 | *) 68 | echo "Error: Unsupported platform '$PLATFORM'" 69 | exit 1 70 | ;; 71 | esac 72 | 73 | # Build $TARGET docs for the $PLATFORM 74 | echo "Building $TARGET docs for $PLATFORM..." 75 | if ! xcodebuild docbuild -scheme $TARGET -derivedDataPath .build/docbuild -destination "generic/platform=$PLATFORM"; then 76 | echo "Error: Failed to build documentation for $PLATFORM" >&2 77 | return 1 78 | fi 79 | 80 | # Transform docs for static hosting 81 | if ! $(xcrun --find docc) process-archive \ 82 | transform-for-static-hosting .build/docbuild/Build/Products/$DEBUG_PATH/$TARGET.doccarchive \ 83 | --output-path .build/docs-$PLATFORM \ 84 | --hosting-base-path "$TARGET"; then 85 | echo "Error: Failed to transform documentation for $PLATFORM" >&2 86 | return 1 87 | fi 88 | 89 | # Inject a root redirect script on the root page 90 | echo "" > .build/docs-$PLATFORM/index.html; 91 | 92 | # Complete successfully 93 | echo "Successfully built $TARGET docs for $PLATFORM" 94 | return 0 95 | } 96 | 97 | # Start script 98 | echo "" 99 | echo "Building $TARGET docs for [$PLATFORMS]..." 100 | echo "" 101 | 102 | # Loop through all platforms and call the build function 103 | for PLATFORM in $PLATFORMS; do 104 | if ! build_platform "$PLATFORM"; then 105 | exit 1 106 | fi 107 | done 108 | 109 | # Complete successfully 110 | echo "" 111 | echo "Building $TARGET docs completed successfully!" 112 | echo "" 113 | -------------------------------------------------------------------------------- /Example/scripts/jazzy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Documentation: 4 | # This script generates jazzy docs for CocoaPods Framework. 5 | 6 | # Usage: 7 | # jazzy.sh 8 | # e.g. `bash scripts/jazzy.sh` 9 | 10 | # Exit immediately if a command exits with non-zero status 11 | set -e 12 | 13 | # Use the script folder to refer to other scripts. 14 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 15 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh" 16 | 17 | # Get package name 18 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; } 19 | LIB_PATH="docs/$PACKAGE_NAME/" 20 | 21 | # Create package directory 22 | rm -rf docs 23 | mkdir docs 24 | mkdir "docs/$PACKAGE_NAME" 25 | 26 | # Generate jazzy docs 27 | sourcekitten doc -- -project Example/Pods/Pods.xcodeproj -target $PACKAGE_NAME > "$LIB_PATH/swift.json" 28 | jazzy --include-spi-declarations --sourcekitten-sourcefile "$LIB_PATH/swift.json" 29 | 30 | # Clean build directory 31 | rm -rf $LIB_PATH 32 | rm -rf Example/build/ 33 | cp *.md docs/ 34 | -------------------------------------------------------------------------------- /Example/scripts/mmkv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 放在与 .xcodeproj 文件同级目录下,编译结果在 build 目录下,输出结果在 Framework 目录下 3 | 4 | # 需要编译的 scheme 5 | scheme="MMKV" 6 | 7 | if [ -z "$scheme" ] || [ "$scheme" = "" ]; then 8 | echo "请填入 scheme 名称" 9 | fi 10 | 11 | echo "scheme: $scheme" 12 | cd "$(dirname "$0")" || exit 0 13 | 14 | xcodebuild archive \ 15 | -scheme "$scheme" \ 16 | -sdk iphoneos \ 17 | -archivePath "build/iphoneos.xcarchive" \ 18 | BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ 19 | SKIP_INSTALL=NO 20 | 21 | xcodebuild archive \ 22 | -scheme "$scheme" \ 23 | -sdk iphonesimulator \ 24 | -archivePath "build/iphonesimulator.xcarchive" \ 25 | BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ 26 | SKIP_INSTALL=NO 27 | 28 | # 优先从 build 文件夹下读取 29 | product_list=$(ls build/iphoneos.xcarchive/Products/Library/Frameworks) 30 | for file_name in $product_list 31 | do 32 | full_product_name=$file_name 33 | break 34 | done 35 | 36 | # 读取不到就从 showBuildSettings 读取 37 | if [ -z "$full_product_name" ] || [ "$full_product_name" = "" ]; then 38 | name_dict=$(xcodebuild -showBuildSettings | grep FULL_PRODUCT_NAME) 39 | full_product_name=${name_dict#*= } 40 | fi 41 | 42 | product_name=${full_product_name%.*} 43 | 44 | xcodebuild -create-xcframework \ 45 | -framework build/iphoneos.xcarchive/Products/Library/Frameworks/"$full_product_name" \ 46 | -framework build/iphonesimulator.xcarchive/Products/Library/Frameworks/"$full_product_name" \ 47 | -output Framework/"$product_name".xcframework 48 | -------------------------------------------------------------------------------- /Example/scripts/package_docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds DocC documentation for `Package.swift`. 5 | # This script targets iOS by default, but you can pass in custom . 6 | 7 | # Usage: 8 | # package_docc.sh [ default:iOS] 9 | # e.g. `bash scripts/package_docc.sh iOS macOS` 10 | 11 | # Exit immediately if a command exits with non-zero status 12 | set -e 13 | 14 | # Use the script folder to refer to other scripts. 15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 16 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh" 17 | SCRIPT_DOCC="$FOLDER/docc.sh" 18 | 19 | # Define platforms variable 20 | if [ $# -eq 0 ]; then 21 | set -- iOS 22 | fi 23 | PLATFORMS=$@ 24 | 25 | # Get package name 26 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; } 27 | 28 | # Build package documentation 29 | bash $SCRIPT_DOCC $PACKAGE_NAME $PLATFORMS || { echo "DocC script failed"; exit 1; } 30 | -------------------------------------------------------------------------------- /Example/scripts/package_framework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script generates an XCFramework for `Package.swift`. 5 | # This script targets iOS by default, but you can pass in custom . 6 | 7 | # Usage: 8 | # package_framework.sh [ default:iOS] 9 | # e.g. `bash scripts/package_framework.sh iOS macOS` 10 | 11 | # Exit immediately if a command exits with non-zero status 12 | set -e 13 | 14 | # Use the script folder to refer to other scripts. 15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 16 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh" 17 | SCRIPT_FRAMEWORK="$FOLDER/framework.sh" 18 | 19 | # Define platforms variable 20 | if [ $# -eq 0 ]; then 21 | set -- iOS 22 | fi 23 | PLATFORMS=$@ 24 | 25 | # Get package name 26 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; } 27 | 28 | # Build package framework 29 | bash $SCRIPT_FRAMEWORK $PACKAGE_NAME $PLATFORMS 30 | -------------------------------------------------------------------------------- /Example/scripts/package_name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script finds the main target name in `Package.swift`. 5 | 6 | # Usage: 7 | # package_name.sh 8 | # e.g. `bash scripts/package_name.sh` 9 | 10 | # Exit immediately if a command exits with non-zero status 11 | set -e 12 | 13 | # Check that a Package.swift file exists 14 | if [ ! -f "Package.swift" ]; then 15 | echo "Error: Package.swift not found in current directory" 16 | exit 1 17 | fi 18 | 19 | # Using grep and sed to extract the package name 20 | # 1. grep finds the line containing "name:" 21 | # 2. sed extracts the text between quotes 22 | package_name=$(grep -m 1 'name:.*"' Package.swift | sed -n 's/.*name:[[:space:]]*"\([^"]*\)".*/\1/p') 23 | 24 | if [ -z "$package_name" ]; then 25 | echo "Error: Could not find package name in Package.swift" 26 | exit 1 27 | else 28 | echo "$package_name" 29 | fi 30 | -------------------------------------------------------------------------------- /Example/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script tests a for all provided . 5 | 6 | # Usage: 7 | # test.sh [ default:iOS macOS tvOS watchOS xrOS] 8 | # e.g. `bash scripts/test.sh MyTarget iOS macOS` 9 | 10 | # Exit immediately if a command exits with a non-zero status 11 | set -e 12 | 13 | # Verify that all required arguments are provided 14 | if [ $# -eq 0 ]; then 15 | echo "Error: This script requires at least one argument" 16 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 17 | echo "For instance: $0 MyTarget iOS macOS" 18 | exit 1 19 | fi 20 | 21 | # Define argument variables 22 | TARGET=$1 23 | 24 | # Remove TARGET from arguments list 25 | shift 26 | 27 | # Define platforms variable 28 | if [ $# -eq 0 ]; then 29 | set -- iOS macOS tvOS watchOS xrOS 30 | fi 31 | PLATFORMS=$@ 32 | 33 | # Start script 34 | echo "" 35 | echo "Testing $TARGET for [$PLATFORMS]..." 36 | echo "" 37 | 38 | # A function that tests $TARGET for a specific platform 39 | test_platform() { 40 | 41 | # Define a local $PLATFORM variable 42 | local PLATFORM="${1//_/ }" 43 | 44 | # Define the destination, based on the $PLATFORM 45 | case $PLATFORM in 46 | "iOS") 47 | DESTINATION="platform=iOS Simulator,name=iPhone 16" 48 | ;; 49 | "macOS") 50 | DESTINATION="platform=macOS" 51 | ;; 52 | "tvOS") 53 | DESTINATION="platform=tvOS Simulator,name=Apple TV" 54 | ;; 55 | "watchOS") 56 | DESTINATION="platform=watchOS Simulator,name=Apple Watch" 57 | ;; 58 | "xrOS") 59 | DESTINATION="platform=xrOS Simulator,name=Apple Vision Pro" 60 | ;; 61 | *) 62 | echo "Error: Unsupported platform '$PLATFORM'" 63 | exit 1 64 | ;; 65 | esac 66 | 67 | # Test $TARGET for the $DESTINATION 68 | echo "Testing $TARGET for $PLATFORM..." 69 | xcodebuild test -scheme $TARGET -derivedDataPath .build -destination "$DESTINATION" -enableCodeCoverage YES; 70 | 71 | # Complete successfully 72 | echo "Successfully tested $TARGET for $PLATFORM" 73 | } 74 | 75 | # Loop through all platforms and call the test function 76 | for PLATFORM in $PLATFORMS; do 77 | if ! test_platform "$PLATFORM"; then 78 | exit 1 79 | fi 80 | done 81 | 82 | # Complete successfully 83 | echo "" 84 | echo "Testing $TARGET completed successfully!" 85 | echo "" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 wuyong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # FWFramework 2 | 3 | [![Pod Version](https://img.shields.io/cocoapods/v/FWFramework.svg?style=flat)](http://cocoadocs.org/docsets/FWFramework/) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Flszzy%2FFWFramework%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/lszzy/FWFramework) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Flszzy%2FFWFramework%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/lszzy/FWFramework) 6 | [![Pod License](https://img.shields.io/cocoapods/l/FWFramework.svg?style=flat)](https://github.com/lszzy/FWFramework/blob/master/LICENSE) 7 | 8 | # [English](https://github.com/lszzy/FWFramework/blob/master/README.md) 9 | 10 | ## 帮助文档 11 | iOS开发框架,主要解决原生开发中的常规和痛点问题,搭建模块化项目架构,方便iOS开发。 12 | 13 | * 模块化架构设计,自带Mediator中间件、Router路由等组件 14 | * 支持Swift协程async、await,属性注解propertyWrapper、宏等高级特性 15 | * 轻松可定制的UI插件,自带弹窗、吐司、空界面、下拉刷新、图片选择等插件 16 | * 完全可替换的网络图片、网络请求层,默认兼容SDWebImage、Alamofire等 17 | * 自动更新的AutoLayout链式布局,常用的UI视图组件一应俱全 18 | * 可扩展的Model、View、Controller架构封装,快速编写业务代码 19 | * 兼容SwiftUI,轻松实现UIKit、SwiftUI混合界面开发 20 | * 兼容Swift 6,快捷编写更健壮不易崩溃、线程安全的代码 21 | * 任意可替换的fw.代码前缀,常用的Toolkit方法、Theme、多语言处理 22 | * 你想要的,这里全都有 23 | 24 | 本框架所有Swizzle默认不会生效,不会对现有项目产生影响,需要手工开启或调用才会生效。本库已经在正式项目使用,后续也会一直维护扩展,欢迎大家使用并提出宝贵意见,共同成长。 25 | 26 | ## 安装教程 27 | 推荐使用CocoaPods或Swift Package Manager安装,自动管理依赖。 28 | 29 | ### CocoaPods 30 | 本框架支持CocoaPods,Podfile示例: 31 | 32 | platform :ios, '13.0' 33 | use_frameworks! 34 | 35 | target 'Example' do 36 | # 引入默认子模块 37 | pod 'FWFramework' 38 | 39 | # 引入指定子模块,子模块列表详见podspec文件 40 | # pod 'FWFramework', :subspecs => ['FWFramework', 'FWUIKit', 'FWSwiftUI', 'FWPlugin/Macros'] 41 | end 42 | 43 | ### Swift Package Manager 44 | 本框架支持Swift Package Manager,添加并勾选所需模块即可,Package示例: 45 | 46 | https://github.com/lszzy/FWFramework.git 47 | 48 | # 勾选并引入默认子模块 49 | import FWFramework 50 | 51 | # 勾选并引入指定子模块,子模块列表详见Package.swift文件 52 | import FWUIKit 53 | import FWSwiftUI 54 | import FWPluginMacros 55 | 56 | ## [Api文档](https://fwframework.wuyong.site) 57 | 文档位于docs文件夹,浏览器打开index.html即可,也可运行docs.sh自动生成Api文档。 58 | 59 | 自定义代码前缀为app示例: 60 | 61 | public typealias APP = WrapperGlobal 62 | 63 | extension WrapperCompatible { 64 | public static var app: Wrapper.Type { get { wrapperExtension } set {} } 65 | public var app: Wrapper { get { wrapperExtension } set {} } 66 | } 67 | 68 | 导入默认fw代码前缀示例: 69 | 70 | @_spi(FW) import FWFramework 71 | 72 | ## [更新日志](https://github.com/lszzy/FWFramework/blob/master/CHANGELOG_CN.md) 73 | 由于本框架一直在升级优化和扩展新功能,各版本Api可能会有些许变动,如果升级新版本时编译报错,解决方案如下: 74 | 75 | 1. 改为指定pod版本号引入即可,推荐方式,不影响项目进度,有空才升级到新版本,示例:pod 'FWFramework', '7.1.0' 76 | 2. 升级迁移到新版本,请留意版本更新日志 77 | 78 | ### Swift 79 | 从6.0版本起,兼容Swift 6,兼容iOS 13+。5.x版本仅兼容Swift 5,迁移时除了使用新API修复编译错误外,还需测试相关功能是否正常,给您带来的不便敬请谅解。 80 | 81 | ### Objective-C 82 | 如需兼容OC,请使用4.x版本,兼容iOS 11+。4.x版本后续只修复bug,不再添加新功能。 83 | 84 | ## 第三方库 85 | 本框架使用了很多第三方库,在此感谢所有第三方库的作者,此处不一一列举,详见源码头文件相关链接。 86 | 87 | 在引入第三方库时,为了兼容现有项目pod依赖,也为了三方库自定义改动和bug修复,并方便后续维护,本框架统一修改了类前缀、方法前缀,使用时如有不便敬请谅解。 88 | 如果您是某三方开源库的作者,若是本库侵犯了您的权益,请告诉我,本人会立即移除该三方开源库的使用,深感歉意。 89 | 90 | ## 官方网站 91 | [大勇的网站](http://www.wuyong.site) 92 | -------------------------------------------------------------------------------- /Sources/FWFramework/Kernel/Benchmark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Benchmark.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/18. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - WrapperGlobal 11 | extension WrapperGlobal { 12 | /// 标记时间调试开始 13 | /// 14 | /// - Parameter name: 调试标签,默认空字符串 15 | public static func begin(_ name: String = "") { 16 | Benchmark.begin(name) 17 | } 18 | 19 | /// 标记时间调试结束并打印消耗时间 20 | /// 21 | /// - Parameter name: 调试标签,默认空字符串 22 | /// - Returns: 消耗时间 23 | @discardableResult 24 | public static func end(_ name: String = "") -> TimeInterval { 25 | Benchmark.end(name) 26 | } 27 | } 28 | 29 | // MARK: - Benchmark 30 | /// 时间调试器 31 | public class Benchmark: @unchecked Sendable { 32 | // MARK: - Accessor 33 | private static let shared = Benchmark() 34 | private var beginTimes: [String: TimeInterval] = [:] 35 | private var endTimes: [String: TimeInterval] = [:] 36 | 37 | // MARK: - Public 38 | /// 标记时间调试开始 39 | public static func begin(_ name: String) { 40 | shared.beginTimes[name] = Date().timeIntervalSince1970 41 | } 42 | 43 | /// 标记时间调试结束并打印消耗时间 44 | @discardableResult 45 | public static func end(_ name: String) -> TimeInterval { 46 | let beginTime = shared.beginTimes[name] ?? Date().timeIntervalSince1970 47 | let endTime = Date().timeIntervalSince1970 48 | shared.endTimes[name] = endTime 49 | 50 | let timeInterval = endTime - beginTime 51 | #if DEBUG 52 | Logger.debug(group: Logger.fw.moduleName, "Benchmark-%@: %.3fms", name, timeInterval * 1000) 53 | #endif 54 | return timeInterval 55 | } 56 | 57 | /// 获取所有的消耗时间数据 58 | public static func benchmarks() -> [String: TimeInterval] { 59 | var times: [String: TimeInterval] = [:] 60 | for (name, endTime) in shared.endTimes { 61 | let beginTime = shared.beginTimes[name] ?? Date().timeIntervalSince1970 62 | times[name] = endTime - beginTime 63 | } 64 | return times 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/FWFramework/Kernel/Bridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bridge.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2024/4/15. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - ObjCBridge 11 | @objc protocol ObjCObjectBridge { 12 | @objc(instanceMethodSignatureForSelector:) 13 | static func objcInstanceMethodSignature(for selector: Selector) -> NSObject & ObjCMethodSignatureBridge 14 | } 15 | 16 | @objc protocol ObjCInvocationBridge { 17 | @objc(selector) 18 | var objcSelector: Selector { get set } 19 | 20 | @objc(target) 21 | var objcTarget: AnyObject? { get set } 22 | 23 | @objc(getReturnValue:) 24 | func objcGetReturnValue(_ retLoc: UnsafeMutableRawPointer?) 25 | 26 | @objc(setArgument:atIndex:) 27 | func objcSetArgument(_ argumentLocation: UnsafeMutableRawPointer?, at index: Int) 28 | 29 | @objc(invoke) 30 | func objcInvoke() 31 | 32 | @objc(invocationWithMethodSignature:) 33 | static func objcInvocation(withMethodSignature signature: AnyObject) -> ObjCInvocationBridge 34 | } 35 | 36 | @objc protocol ObjCMethodSignatureBridge { 37 | @objc(numberOfArguments) 38 | var objcNumberOfArguments: UInt { get } 39 | 40 | @objc(methodReturnType) 41 | var objcMethodReturnType: UnsafePointer { get } 42 | 43 | @objc(getArgumentTypeAtIndex:) 44 | func objcGetArgumentType(at index: UInt) -> UnsafePointer 45 | 46 | @objc(signatureWithObjCTypes:) 47 | static func objcSignature(withObjCTypes typeEncoding: UnsafePointer) -> AnyObject 48 | } 49 | 50 | enum ObjCClassBridge { 51 | static let invocationClass: AnyClass? = NSClassFromString("NSInvocation") 52 | static let methodSignatureClass: AnyClass? = NSClassFromString("NSMethodSignature") 53 | 54 | static let forwardInvocationSelector = NSSelectorFromString("forwardInvocation:") 55 | static let methodSignatureSelector = NSSelectorFromString("methodSignatureForSelector:") 56 | } 57 | 58 | enum ObjCTypeEncodingBridge: Int8 { 59 | case char = 99 60 | case int = 105 61 | case short = 115 62 | case long = 108 63 | case longLong = 113 64 | 65 | case unsignedChar = 67 66 | case unsignedInt = 73 67 | case unsignedShort = 83 68 | case unsignedLong = 76 69 | case unsignedLongLong = 81 70 | 71 | case float = 102 72 | case double = 100 73 | 74 | case bool = 66 75 | 76 | case object = 64 77 | case type = 35 78 | case selector = 58 79 | 80 | case undefined = -1 81 | } 82 | -------------------------------------------------------------------------------- /Sources/FWFramework/Kernel/Parameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameter.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2024/4/15. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - AnyParameter 11 | public protocol AnyParameter {} 12 | 13 | public protocol DataParameter: AnyParameter { 14 | var dataValue: Data { get } 15 | } 16 | 17 | public protocol StringParameter: AnyParameter { 18 | var stringValue: String { get } 19 | } 20 | 21 | public protocol AttributedStringParameter: AnyParameter { 22 | var attributedStringValue: NSAttributedString { get } 23 | } 24 | 25 | public protocol URLParameter: AnyParameter { 26 | var urlValue: URL { get } 27 | } 28 | 29 | public protocol ArrayParameter: AnyParameter { 30 | associatedtype E 31 | var arrayValue: [E] { get } 32 | } 33 | 34 | public protocol DictionaryParameter: AnyParameter where K: Hashable { 35 | associatedtype K 36 | associatedtype V 37 | var dictionaryValue: [K: V] { get } 38 | } 39 | 40 | public protocol ObjectParameter: DictionaryParameter, ObjectType { 41 | init(dictionaryValue: [AnyHashable: Any]) 42 | } 43 | 44 | // MARK: - AnyParameter+Extension 45 | extension Data: DataParameter, StringParameter { 46 | public var dataValue: Data { self } 47 | public var stringValue: String { String(data: self, encoding: .utf8) ?? .init() } 48 | } 49 | 50 | extension String: StringParameter, AttributedStringParameter, DataParameter, URLParameter { 51 | public var stringValue: String { self } 52 | public var attributedStringValue: NSAttributedString { NSAttributedString(string: self) } 53 | public var dataValue: Data { data(using: .utf8) ?? .init() } 54 | public var urlValue: URL { URL.fw.url(string: self) ?? URL() } 55 | } 56 | 57 | extension NSAttributedString: AttributedStringParameter, StringParameter { 58 | public var attributedStringValue: NSAttributedString { self } 59 | public var stringValue: String { string } 60 | } 61 | 62 | extension URL: URLParameter, StringParameter { 63 | public var urlValue: URL { self } 64 | public var stringValue: String { absoluteString } 65 | } 66 | 67 | extension URLRequest: URLParameter, StringParameter { 68 | public var urlValue: URL { url ?? URL() } 69 | public var stringValue: String { url?.absoluteString ?? .init() } 70 | } 71 | 72 | extension Array: ArrayParameter { 73 | public var arrayValue: [Element] { self } 74 | } 75 | 76 | extension Dictionary: DictionaryParameter { 77 | public var dictionaryValue: [Key: Value] { self } 78 | } 79 | 80 | extension ObjectParameter { 81 | public var dictionaryValue: [AnyHashable: Any] { 82 | NSObject.fw.mirrorDictionary(self) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/FWFramework/Kernel/Wrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Wrapper.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/18. 6 | // 7 | 8 | import Foundation 9 | import QuartzCore 10 | 11 | // MARK: - WrapperGlobal 12 | /// 全局包装器 13 | /// 14 | /// 自定义WrapperGlobal为任意名称(如APP)示例: 15 | /// ```swift 16 | /// public typealias APP = WrapperGlobal 17 | /// ``` 18 | /// 使用示例: 19 | /// ```swift 20 | /// APP.safeString(object) 21 | /// ``` 22 | public class WrapperGlobal { 23 | /// 当前框架版本号 24 | public static let version = "7.1.0" 25 | } 26 | 27 | /// 全局包装器别名 28 | @_spi(FW) public typealias FW = WrapperGlobal 29 | 30 | // MARK: - Wrapper 31 | /// 属性包装器 32 | public class Wrapper { 33 | /// 原始对象 34 | public let base: Base 35 | 36 | /// 初始化方法 37 | public init(_ base: Base) { 38 | self.base = base 39 | } 40 | } 41 | 42 | // MARK: - WrapperCompatible 43 | /// 属性包装器兼容协议 44 | /// 45 | /// 自定义wrapperExtension为任意名称(如app)示例: 46 | /// ```swift 47 | /// extension WrapperCompatible { 48 | /// public static var app: Wrapper.Type { get { wrapperExtension } set {} } 49 | /// public var app: Wrapper { get { wrapperExtension } set {} } 50 | /// } 51 | /// ``` 52 | /// 使用示例: 53 | /// ```swift 54 | /// String.app.jsonEncode(object) 55 | /// ``` 56 | public protocol WrapperCompatible { 57 | /// 关联类型 58 | associatedtype WrapperBase 59 | 60 | /// wrapperExtension类包装器属性 61 | static var wrapperExtension: Wrapper.Type { get set } 62 | /// wrapperExtension对象包装器属性 63 | var wrapperExtension: Wrapper { get set } 64 | } 65 | 66 | /// 注意事项: 67 | /// 1. 静态扩展方法中尽量不使用Base.self,因为可能会出现类型与预期不一致的场景。 68 | /// 示例1:Logger.fw.method(),此时method中Base.self为Logger,预期结果正确 69 | /// 示例2:ModuleBundle类 class var 实现时使用 self.fw.method(),此时子类method中Base.self可能为父类,与预期结果不一致 70 | /// 2. 扩展方法闭包中请勿使用[weak self],而应该使用[weak base],因为self使用完就会释放;闭包如需强引用base,可外部声明let strongBase = base,再在内部使用strongBase即可;详情可参见Block实现 71 | extension WrapperCompatible { 72 | /// wrapperExtension类包装器属性 73 | public static var wrapperExtension: Wrapper.Type { 74 | get { Wrapper.self } 75 | set {} 76 | } 77 | 78 | /// wrapperExtension对象包装器属性 79 | public var wrapperExtension: Wrapper { 80 | get { Wrapper(self) } 81 | set {} 82 | } 83 | 84 | /// fw类包装器属性 85 | @_spi(FW) public static var fw: Wrapper.Type { 86 | get { Wrapper.self } 87 | set {} 88 | } 89 | 90 | /// fw对象包装器属性 91 | @_spi(FW) public var fw: Wrapper { 92 | get { Wrapper(self) } 93 | set {} 94 | } 95 | } 96 | 97 | // MARK: - WrapperObject 98 | /// 属性包装器对象,用于扩展AnyObject 99 | /// 100 | /// 注意事项: 101 | /// 1. 需要AnyObject通用的才扩展WrapperObject,否则扩展NSObject 102 | /// 2. 静态static方法需要使用self的才扩展WrapperObject,否则扩展NSObject 103 | /// 3. 扩展WrapperObject时如需使用static var变量,可借助NSObject的fileprivate扩展 104 | public typealias WrapperObject = AnyObject & WrapperCompatible 105 | 106 | // MARK: - WrapperCompatible 107 | extension Int: WrapperCompatible {} 108 | extension Int8: WrapperCompatible {} 109 | extension Int16: WrapperCompatible {} 110 | extension Int32: WrapperCompatible {} 111 | extension Int64: WrapperCompatible {} 112 | extension UInt: WrapperCompatible {} 113 | extension UInt8: WrapperCompatible {} 114 | extension UInt16: WrapperCompatible {} 115 | extension UInt32: WrapperCompatible {} 116 | extension UInt64: WrapperCompatible {} 117 | extension Float: WrapperCompatible {} 118 | extension Double: WrapperCompatible {} 119 | extension Bool: WrapperCompatible {} 120 | extension String: WrapperCompatible {} 121 | extension Data: WrapperCompatible {} 122 | extension Date: WrapperCompatible {} 123 | extension URL: WrapperCompatible {} 124 | extension Array: WrapperCompatible {} 125 | extension Set: WrapperCompatible {} 126 | extension Dictionary: WrapperCompatible {} 127 | extension Decimal: WrapperCompatible {} 128 | extension CGFloat: WrapperCompatible {} 129 | extension CGPoint: WrapperCompatible {} 130 | extension CGSize: WrapperCompatible {} 131 | extension CGRect: WrapperCompatible {} 132 | extension NSObject: WrapperCompatible {} 133 | -------------------------------------------------------------------------------- /Sources/FWFramework/Service/Cache/CacheFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheFile.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2023/3/29. 6 | // 7 | 8 | import CommonCrypto 9 | import Foundation 10 | 11 | /// 文件缓存。复杂对象需遵循NSCoding|AnyArchivable协议 12 | open class CacheFile: CacheEngine, @unchecked Sendable { 13 | /// 单例模式 14 | public static let shared = CacheFile() 15 | 16 | /// 缓存根目录路径 17 | public private(set) var cachePath: String = "" 18 | 19 | override public convenience init() { 20 | self.init(path: nil) 21 | } 22 | 23 | /// 指定路径 24 | public init(path: String?) { 25 | super.init() 26 | // 绝对路径: path 27 | if let path, (path as NSString).isAbsolutePath { 28 | self.cachePath = path 29 | // 相对路径: Libray/Caches/FWFramework/CacheFile/path[shared] 30 | } else { 31 | let cachePath = FileManager.fw.pathCaches.fw.appendingPath(["FWFramework", "CacheFile"]) 32 | let fileName = path ?? "" 33 | self.cachePath = cachePath.fw.appendingPath(!fileName.isEmpty ? fileName : "shared") 34 | } 35 | } 36 | 37 | private func filePath(_ key: String) -> String { 38 | let fileName = "\(key.fw.md5Encode).plist" 39 | return (cachePath as NSString).appendingPathComponent(fileName) 40 | } 41 | 42 | // MARK: - CacheEngineProtocol 43 | override open func readCache(forKey key: String) -> T? { 44 | let filePath = filePath(key) 45 | if FileManager.default.fileExists(atPath: filePath) { 46 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { return nil } 47 | return data.fw.unarchivedObject(as: T.self) 48 | } 49 | return nil 50 | } 51 | 52 | override open func writeCache(_ object: T, forKey key: String) { 53 | let filePath = filePath(key) 54 | // 自动创建目录 55 | let fileDir = (filePath as NSString).deletingLastPathComponent 56 | if !FileManager.default.fileExists(atPath: fileDir) { 57 | try? FileManager.default.createDirectory(atPath: fileDir, withIntermediateDirectories: true, attributes: nil) 58 | } 59 | guard let data = Data.fw.archivedData(object) else { return } 60 | try? data.write(to: URL(fileURLWithPath: filePath)) 61 | } 62 | 63 | override open func clearCache(forKey key: String) { 64 | let filePath = filePath(key) 65 | try? FileManager.default.removeItem(atPath: filePath) 66 | } 67 | 68 | override open func clearAllCaches() { 69 | try? FileManager.default.removeItem(atPath: cachePath) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/FWFramework/Service/Cache/CacheKeychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheKeychain.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2023/3/29. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Keychain缓存。复杂对象需遵循NSCoding|AnyArchivable协议 11 | open class CacheKeychain: CacheEngine, @unchecked Sendable { 12 | /// 单例模式 13 | public static let shared = CacheKeychain() 14 | 15 | private var group: String? 16 | private var service = "FWCache" 17 | 18 | override public init() { 19 | super.init() 20 | } 21 | 22 | /// 分组对象 23 | public init(group: String?, service: String? = nil) { 24 | super.init() 25 | self.group = group 26 | if let service, !service.isEmpty { 27 | self.service = service 28 | } 29 | } 30 | 31 | // MARK: - CacheEngineProtocol 32 | override open func readCache(forKey key: String) -> T? { 33 | passwordObject(forService: service, account: key) 34 | } 35 | 36 | override open func writeCache(_ object: T, forKey key: String) { 37 | setPasswordObject(object, forService: service, account: key) 38 | } 39 | 40 | override open func clearCache(forKey key: String) { 41 | deletePassword(forService: service, account: key) 42 | } 43 | 44 | override open func clearAllCaches() { 45 | deletePassword(forService: service, account: nil) 46 | } 47 | 48 | // MARK: - Private 49 | private func passwordData(forService service: String, account: String?) -> Data? { 50 | var result: AnyObject? 51 | var query = query(forService: service, account: account) 52 | query[kSecReturnData as String] = kCFBooleanTrue 53 | query[kSecMatchLimit as String] = kSecMatchLimitOne 54 | 55 | let status = SecItemCopyMatching(query as CFDictionary, &result) 56 | if status != errSecSuccess { 57 | return nil 58 | } 59 | return result as? Data 60 | } 61 | 62 | private func passwordObject(forService service: String, account: String?) -> T? { 63 | guard let passwordData = passwordData(forService: service, account: account) else { 64 | return nil 65 | } 66 | 67 | return passwordData.fw.unarchivedObject(as: T.self) 68 | } 69 | 70 | @discardableResult 71 | private func setPasswordData(_ passwordData: Data, forService service: String, account: String?) -> Bool { 72 | let searchQuery = query(forService: service, account: account) 73 | var status = SecItemCopyMatching(searchQuery as CFDictionary, nil) 74 | 75 | // 更新数据 76 | if status == errSecSuccess { 77 | var query = [String: Any]() 78 | query[kSecValueData as String] = passwordData 79 | status = SecItemUpdate(searchQuery as CFDictionary, query as CFDictionary) 80 | // 添加数据 81 | } else if status == errSecItemNotFound { 82 | var query = query(forService: service, account: account) 83 | query[kSecValueData as String] = passwordData 84 | status = SecItemAdd(query as CFDictionary, nil) 85 | } 86 | return status == errSecSuccess 87 | } 88 | 89 | @discardableResult 90 | private func setPasswordObject(_ passwordObject: T, forService service: String, account: String?) -> Bool { 91 | guard let passwordData = Data.fw.archivedData(passwordObject) else { return false } 92 | return setPasswordData(passwordData, forService: service, account: account) 93 | } 94 | 95 | @discardableResult 96 | private func deletePassword(forService service: String, account: String?) -> Bool { 97 | let query = query(forService: service, account: account) 98 | let status = SecItemDelete(query as CFDictionary) 99 | return status == errSecSuccess 100 | } 101 | 102 | private func query(forService service: String, account: String?) -> [String: Any] { 103 | var query = [String: Any]() 104 | query[kSecClass as String] = kSecClassGenericPassword 105 | query[kSecAttrService as String] = service 106 | if let account { 107 | query[kSecAttrAccount as String] = account 108 | } 109 | if let group { 110 | query[kSecAttrAccessGroup as String] = group 111 | } 112 | return query 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/FWFramework/Service/Cache/CacheMemory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheMemory.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2023/3/29. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 内存缓存 11 | open class CacheMemory: CacheEngine, @unchecked Sendable { 12 | /// 单例模式 13 | public static let shared = CacheMemory() 14 | 15 | private var cachePool = [String: Any]() 16 | 17 | override open func readCache(forKey key: String) -> T? { 18 | cachePool[key] as? T 19 | } 20 | 21 | override open func writeCache(_ object: T, forKey key: String) { 22 | cachePool[key] = object 23 | } 24 | 25 | override open func clearCache(forKey key: String) { 26 | cachePool.removeValue(forKey: key) 27 | } 28 | 29 | override open func clearAllCaches() { 30 | cachePool.removeAll() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/FWFramework/Service/Cache/CacheUserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheUserDefaults.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2023/3/29. 6 | // 7 | 8 | import Foundation 9 | 10 | /// UserDefaults缓存。复杂对象需遵循AnyArchivable协议 11 | open class CacheUserDefaults: CacheEngine, @unchecked Sendable { 12 | /// 单例模式 13 | public static let shared = CacheUserDefaults() 14 | 15 | private let userDefaults: UserDefaults 16 | 17 | /// 初始化 18 | override public init() { 19 | self.userDefaults = UserDefaults.standard 20 | super.init() 21 | } 22 | 23 | /// 分组对象 24 | public init(group: String?) { 25 | self.userDefaults = UserDefaults(suiteName: group) ?? .standard 26 | super.init() 27 | } 28 | 29 | /// 和非缓存Key区分开,防止清除非缓存信息 30 | private func cacheKey(_ key: String) -> String { 31 | "FWCache.\(key)" 32 | } 33 | 34 | // MARK: - CacheEngineProtocol 35 | override open func readCache(forKey key: String) -> T? { 36 | var value = userDefaults.object(forKey: cacheKey(key)) 37 | if let data = value as? Data, let coder = ArchiveCoder.unarchivedCoder(data) { 38 | value = coder.archivableObject(as: T.self) 39 | } 40 | return value as? T 41 | } 42 | 43 | override open func writeCache(_ object: T, forKey key: String) { 44 | var value: Any? = object 45 | if ArchiveCoder.isArchivableObject(object) { 46 | value = Data.fw.archivedData(object) 47 | } 48 | userDefaults.set(value, forKey: cacheKey(key)) 49 | userDefaults.synchronize() 50 | } 51 | 52 | override open func clearCache(forKey key: String) { 53 | userDefaults.removeObject(forKey: cacheKey(key)) 54 | userDefaults.synchronize() 55 | } 56 | 57 | override open func clearAllCaches() { 58 | let dict = userDefaults.dictionaryRepresentation() 59 | for key in dict.keys { 60 | if key.hasPrefix("FWCache.") { 61 | userDefaults.removeObject(forKey: key) 62 | } 63 | } 64 | userDefaults.synchronize() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/FWFramework/Toolkit/Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Appearance.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - Wrapper+NSObject 11 | extension Wrapper where Base: NSObject { 12 | /// 从 appearance 里取值并赋值给当前实例,通常在对象的 init 里调用。支持的属性需标记为\@objc dynamic才生效 13 | public func applyAppearance() { 14 | let aClass: AnyClass = type(of: base) 15 | guard aClass.responds(to: NSSelectorFromString("appearance")) else { return } 16 | 17 | let appearanceGuideClassSelector = NSSelectorFromString(String(format: "%@%@%@", "_a", "ppearanceG", "uideClass")) 18 | if !class_respondsToSelector(aClass, appearanceGuideClassSelector) { 19 | let typeEncoding = method_getTypeEncoding(class_getInstanceMethod(UIView.self, appearanceGuideClassSelector)!) 20 | let impBlock: @convention(block) () -> AnyClass? = { nil } 21 | class_addMethod(aClass, appearanceGuideClassSelector, imp_implementationWithBlock(impBlock), typeEncoding) 22 | } 23 | 24 | let selector = NSSelectorFromString(String(format: "_%@:%@:", "applyInvocationsTo", "window")) 25 | if let appearanceClass = NSClassFromString(String(format: "%@%@%@", "_U", "IAppea", "rance")), 26 | appearanceClass.responds(to: selector) { 27 | _ = (appearanceClass as AnyObject).perform(selector, with: base, with: nil) 28 | } 29 | } 30 | } 31 | 32 | // MARK: - Appearance 33 | /// UIAppearance扩展类,支持任意NSObject对象使用UIAppearance能力 34 | /// 35 | /// 系统默认时机是在didMoveToWindow处理UIAppearance 36 | /// 注意:Swift只有标记\@objc dynamic的属性才支持UIAppearance 37 | /// [QMUI_iOS](https://github.com/Tencent/QMUI_iOS) 38 | public class Appearance { 39 | /// 获取指定 Class 的 appearance 对象,每个 Class 全局只会存在一个 appearance 对象 40 | public static func appearance(for aClass: AnyClass) -> AnyObject? { 41 | let className = NSStringFromClass(aClass) 42 | if let appearance = FrameworkConfiguration.classAppearances[className] { 43 | return appearance 44 | } 45 | 46 | let selector = NSSelectorFromString(String(format: "_%@:%@:", "appearanceForClass", "withContainerList")) 47 | guard let appearanceClass = NSClassFromString(String(format: "%@%@%@", "_U", "IAppea", "rance")), 48 | appearanceClass.responds(to: selector), 49 | let appearance = (appearanceClass as AnyObject).perform(selector, with: aClass, with: nil)?.takeUnretainedValue() else { 50 | return nil 51 | } 52 | 53 | FrameworkConfiguration.classAppearances[className] = appearance 54 | return appearance 55 | } 56 | 57 | /// 获取指定 appearance 对象的关联 Class,通过解析_UIAppearance对象获取 58 | public static func `class`(for appearance: AnyObject) -> AnyClass { 59 | var selector = NSSelectorFromString(String(format: "_%@%@", "customizable", "ClassInfo")) 60 | guard appearance.responds(to: selector) else { 61 | return type(of: appearance) 62 | } 63 | 64 | let classInfo = appearance.perform(selector)?.takeUnretainedValue() as? AnyObject 65 | selector = NSSelectorFromString(String(format: "_%@%@", "customizable", "ViewClass")) 66 | guard let classInfo, classInfo.responds(to: selector) else { 67 | return type(of: appearance) 68 | } 69 | 70 | let viewClass: AnyClass? = classInfo.perform(selector)?.takeUnretainedValue() as? AnyClass 71 | if let viewClass, object_isClass(viewClass) { 72 | return viewClass 73 | } 74 | return type(of: appearance) 75 | } 76 | } 77 | 78 | // MARK: - FrameworkConfiguration+Appearance 79 | extension FrameworkConfiguration { 80 | fileprivate static var classAppearances: [String: AnyObject] = [:] 81 | } 82 | -------------------------------------------------------------------------------- /Sources/FWFramework/Toolkit/Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Chainable.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2024/8/17. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - AnyChainable 11 | public protocol AnyChainable {} 12 | 13 | extension NSObject: AnyChainable {} 14 | 15 | extension AnyChainable { 16 | /// 链式方式指定keyPath对应值,返回新对象 17 | public func chainValue( 18 | _ keyPath: WritableKeyPath, 19 | _ value: Value 20 | ) -> Self { 21 | var result = self 22 | result[keyPath: keyPath] = value 23 | return result 24 | } 25 | 26 | /// 链式方式调用句柄,返回新对象 27 | public func chainBlock( 28 | _ closure: (inout Self) -> Void 29 | ) -> Self { 30 | var result = self 31 | closure(&result) 32 | return result 33 | } 34 | } 35 | 36 | extension AnyChainable where Self: AnyObject { 37 | /// 链式方式指定keyPath对应值,返回自身 38 | @discardableResult 39 | public func chainValue( 40 | _ keyPath: ReferenceWritableKeyPath, 41 | _ value: Value 42 | ) -> Self { 43 | self[keyPath: keyPath] = value 44 | return self 45 | } 46 | 47 | /// 链式方式调用句柄,返回自身 48 | @discardableResult 49 | public func chainBlock( 50 | _ closure: (Self) -> Void 51 | ) -> Self { 52 | closure(self) 53 | return self 54 | } 55 | } 56 | 57 | // MARK: - Dictionary+Chainable 58 | extension Dictionary { 59 | /// 链式方式指定key对应值,返回新字典 60 | public func chainValue( 61 | _ key: Key, 62 | _ value: Value? 63 | ) -> Self { 64 | var result = self 65 | result[key] = value 66 | return result 67 | } 68 | 69 | /// 链式方式调用句柄,返回新字典 70 | public func chainBlock( 71 | _ closure: (inout Self) -> Void 72 | ) -> Self { 73 | var result = self 74 | closure(&result) 75 | return result 76 | } 77 | } 78 | 79 | // MARK: - UIView+ResultBuilder 80 | /// UIView兼容ArrayResultBuilder 81 | public protocol ArrayResultBuilderCompatible {} 82 | 83 | extension UIView: ArrayResultBuilderCompatible {} 84 | 85 | @MainActor extension ArrayResultBuilderCompatible where Self: UIView { 86 | /// 初始化并批量配置子视图 87 | public init( 88 | frame: CGRect = .zero, 89 | @ArrayResultBuilder subviews: () -> [UIView] 90 | ) { 91 | self.init(frame: frame) 92 | arrangeSubviews(subviews) 93 | } 94 | 95 | /// 批量配置子视图,支持链式调用 96 | @discardableResult 97 | public func arrangeSubviews( 98 | @ArrayResultBuilder _ subviews: () -> [UIView] 99 | ) -> Self { 100 | for view in subviews() { 101 | if view.superview == nil { 102 | addSubview(view) 103 | } 104 | } 105 | return self 106 | } 107 | 108 | /// 调用布局句柄,支持链式调用 109 | @discardableResult 110 | public func arrangeLayout( 111 | _ block: (Self) -> Void 112 | ) -> Self { 113 | block(self) 114 | return self 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/FWFramework/Toolkit/Keychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keychain.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | 11 | // MARK: - KeychainManager 12 | /// Keychain管理器 13 | public class KeychainManager: @unchecked Sendable { 14 | // MARK: - Accessor 15 | /// 单例模式 16 | public static let shared = KeychainManager() 17 | 18 | private var group: String? 19 | 20 | // MARK: - Lifecycle 21 | /// 公用对象 22 | public init() {} 23 | 24 | /// 分组对象 25 | public init(group: String?) { 26 | self.group = group 27 | } 28 | 29 | // MARK: - Public 30 | /// 读取String数据 31 | public func password(forService service: String?, account: String?) -> String? { 32 | guard let passwordData = passwordData(forService: service, account: account) else { return nil } 33 | return String(data: passwordData, encoding: .utf8) 34 | } 35 | 36 | /// 读取Data数据 37 | public func passwordData(forService service: String?, account: String?) -> Data? { 38 | var query = query(forService: service, account: account) 39 | query[String(kSecReturnData)] = kCFBooleanTrue 40 | query[String(kSecMatchLimit)] = kSecMatchLimitOne 41 | 42 | var result: AnyObject? 43 | let status = SecItemCopyMatching(query as CFDictionary, &result) 44 | return status == errSecSuccess ? result as? Data : nil 45 | } 46 | 47 | /// 读取Object数据 48 | public func passwordObject(forService service: String?, account: String?) -> Any? { 49 | guard let passwordData = passwordData(forService: service, account: account) else { return nil } 50 | return passwordData.fw.unarchivedObject() 51 | } 52 | 53 | /// 保存String数据 54 | @discardableResult 55 | public func setPassword(_ password: String, forService service: String?, account: String?) -> Bool { 56 | guard let passwordData = password.data(using: .utf8) else { return false } 57 | return setPasswordData(passwordData, forService: service, account: account) 58 | } 59 | 60 | /// 保存Data数据 61 | @discardableResult 62 | public func setPasswordData(_ passwordData: Data, forService service: String?, account: String?) -> Bool { 63 | let searchQuery = query(forService: service, account: account) 64 | var status = SecItemCopyMatching(searchQuery as CFDictionary, nil) 65 | // 更新数据 66 | if status == errSecSuccess { 67 | var query: [String: Any] = [:] 68 | query[String(kSecValueData)] = passwordData 69 | status = SecItemUpdate(searchQuery as CFDictionary, query as CFDictionary) 70 | // 更新数据 71 | } else if status == errSecItemNotFound { 72 | var query = query(forService: service, account: account) 73 | query[String(kSecValueData)] = passwordData 74 | status = SecItemAdd(query as CFDictionary, nil) 75 | } 76 | return status == errSecSuccess 77 | } 78 | 79 | /// 保存Object数据 80 | @discardableResult 81 | public func setPasswordObject(_ passwordObject: Any, forService service: String?, account: String?) -> Bool { 82 | guard let passwordData = Data.fw.archivedData(passwordObject) else { return false } 83 | return setPasswordData(passwordData, forService: service, account: account) 84 | } 85 | 86 | /// 删除数据 87 | @discardableResult 88 | public func deletePassword(forService service: String?, account: String?) -> Bool { 89 | let query = query(forService: service, account: account) 90 | let status = SecItemDelete(query as CFDictionary) 91 | return status == errSecSuccess 92 | } 93 | 94 | // MARK: - Private 95 | private func query(forService service: String?, account: String?) -> [String: Any] { 96 | var query: [String: Any] = [:] 97 | query[String(kSecClass)] = kSecClassGenericPassword 98 | if let service { query[String(kSecAttrService)] = service } 99 | if let account { query[String(kSecAttrAccount)] = account } 100 | if let group { query[String(kSecAttrAccessGroup)] = group } 101 | return query 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/FWFramework/Toolkit/ResultBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultBuilder.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2024/8/26. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - ArrayResultBuilder 11 | /// 常用ArrayResultBuilder 12 | /// 13 | /// [SwiftUIX](https://github.com/swiftuix/SwiftUIX) 14 | @resultBuilder 15 | public struct ArrayResultBuilder { 16 | @_optimize(speed) @_transparent 17 | public static func buildBlock() -> [Element] { 18 | [] 19 | } 20 | 21 | @_optimize(speed) @_transparent 22 | public static func buildBlock(_ element: Element) -> [Element] { 23 | [element] 24 | } 25 | 26 | @_optimize(speed) @_transparent 27 | public static func buildBlock(_ elements: Element...) -> [Element] { 28 | elements 29 | } 30 | 31 | @_optimize(speed) @_transparent 32 | public static func buildBlock(_ arrays: [Element]...) -> [Element] { 33 | arrays.flatMap { $0 } 34 | } 35 | 36 | @_optimize(speed) @_transparent 37 | public static func buildEither(first component: Element) -> [Element] { 38 | [component] 39 | } 40 | 41 | @_optimize(speed) @_transparent 42 | public static func buildEither(first component: [Element]) -> [Element] { 43 | component 44 | } 45 | 46 | @_optimize(speed) @_transparent 47 | public static func buildEither(second component: [Element]) -> [Element] { 48 | component 49 | } 50 | 51 | @_optimize(speed) @_transparent 52 | public static func buildExpression(_ element: Element) -> [Element] { 53 | [element] 54 | } 55 | 56 | @_optimize(speed) @_transparent 57 | public static func buildExpression(_ element: Element?) -> [Element] { 58 | element.map { [$0] } ?? [] 59 | } 60 | 61 | @_optimize(speed) @_transparent 62 | public static func buildExpression(_ elements: [Element]) -> [Element] { 63 | elements 64 | } 65 | 66 | @_optimize(speed) @_transparent 67 | public static func buildOptional(_ component: [Element]?) -> [Element] { 68 | component ?? [] 69 | } 70 | 71 | @_optimize(speed) @_transparent 72 | public static func buildArray(_ contents: [[Element]]) -> [Element] { 73 | contents.flatMap { $0 } 74 | } 75 | } 76 | 77 | // MARK: - NSMutableAttributedString+ResultBuilder 78 | extension NSMutableAttributedString { 79 | /// 拼接NSAttributedString 80 | public static func concatenate( 81 | @ArrayResultBuilder _ items: () -> [AttributedStringParameter] 82 | ) -> Self { 83 | let result = Self() 84 | for item in items() { 85 | result.append(item.attributedStringValue) 86 | } 87 | return result 88 | } 89 | 90 | /// 初始化并拼接NSAttributedString 91 | public convenience init( 92 | @ArrayResultBuilder _ items: () -> [AttributedStringParameter] 93 | ) { 94 | self.init() 95 | concatenate(items) 96 | } 97 | 98 | /// 拼接NSAttributedString 99 | @discardableResult 100 | public func concatenate( 101 | @ArrayResultBuilder _ items: () -> [AttributedStringParameter] 102 | ) -> Self { 103 | for item in items() { 104 | append(item.attributedStringValue) 105 | } 106 | return self 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Authorize/Calendar/AuthorizeCalendar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizeCalendar.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import EventKit 9 | #if FWMacroSPM 10 | @_spi(FW) import FWFramework 11 | #endif 12 | 13 | // MARK: - AuthorizeType+Calendar 14 | extension AuthorizeType { 15 | /// 日历,Info.plist需配置NSCalendarsUsageDescription|NSCalendarsFullAccessUsageDescription 16 | public static let calendars: AuthorizeType = .init("calendars") 17 | /// 日历仅写入,Info.plist需配置NSCalendarsUsageDescription|NSCalendarsWriteOnlyAccessUsageDescription 18 | public static let calendarsWriteOnly: AuthorizeType = .init("calendarsWriteOnly") 19 | /// 提醒,Info.plist需配置NSRemindersUsageDescription|NSRemindersFullAccessUsageDescription 20 | public static let reminders: AuthorizeType = .init("reminders") 21 | } 22 | 23 | // MARK: - AuthorizeCalendar 24 | /// 日历授权 25 | public class AuthorizeCalendar: NSObject, AuthorizeProtocol, @unchecked Sendable { 26 | public static let shared = AuthorizeCalendar(type: .event) 27 | public static let writeOnly = AuthorizeCalendar(type: .event, writeOnly: true) 28 | public static let reminder = AuthorizeCalendar(type: .reminder) 29 | 30 | private var type: EKEntityType = .event 31 | private var writeOnly: Bool = false 32 | 33 | public init(type: EKEntityType, writeOnly: Bool = false) { 34 | super.init() 35 | self.type = type 36 | self.writeOnly = writeOnly 37 | } 38 | 39 | public func authorizeStatus() -> AuthorizeStatus { 40 | let status = EKEventStore.authorizationStatus(for: type) 41 | switch status { 42 | case .restricted: 43 | return .restricted 44 | case .denied: 45 | return .denied 46 | case .authorized: 47 | return .authorized 48 | #if swift(>=5.9) 49 | case .fullAccess: 50 | return .authorized 51 | case .writeOnly: 52 | if #available(iOS 17.0, *) { 53 | if type == .event && !writeOnly { 54 | return .denied 55 | } 56 | } 57 | return .authorized 58 | #endif 59 | default: 60 | return .notDetermined 61 | } 62 | } 63 | 64 | public func requestAuthorize(_ completion: (@MainActor @Sendable (AuthorizeStatus, Error?) -> Void)?) { 65 | let completionHandler: EKEventStoreRequestAccessCompletionHandler = { granted, error in 66 | let status: AuthorizeStatus = granted ? .authorized : .denied 67 | if completion != nil { 68 | DispatchQueue.fw.mainAsync { 69 | completion?(status, error) 70 | } 71 | } 72 | } 73 | 74 | #if swift(>=5.9) 75 | if #available(iOS 17.0, *) { 76 | let eventStore = EKEventStore() 77 | if type == .event { 78 | if writeOnly { 79 | eventStore.requestWriteOnlyAccessToEvents(completion: completionHandler) 80 | } else { 81 | eventStore.requestFullAccessToEvents(completion: completionHandler) 82 | } 83 | } else { 84 | eventStore.requestFullAccessToReminders(completion: completionHandler) 85 | } 86 | return 87 | } 88 | #endif 89 | 90 | let eventStore = EKEventStore() 91 | eventStore.requestAccess(to: type, completion: completionHandler) 92 | } 93 | } 94 | 95 | // MARK: - Autoloader+Calendar 96 | @objc extension Autoloader { 97 | static func loadPlugin_Calendar() { 98 | AuthorizeManager.presetAuthorize(.calendars) { AuthorizeCalendar.shared } 99 | AuthorizeManager.presetAuthorize(.calendarsWriteOnly) { AuthorizeCalendar.writeOnly } 100 | AuthorizeManager.presetAuthorize(.reminders) { AuthorizeCalendar.reminder } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Authorize/Contacts/AuthorizeContacts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizeContacts.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import Contacts 9 | #if FWMacroSPM 10 | @_spi(FW) import FWFramework 11 | #endif 12 | 13 | // MARK: - AuthorizeType+Contacts 14 | extension AuthorizeType { 15 | /// 联系人,Info.plist需配置NSContactsUsageDescription 16 | public static let contacts: AuthorizeType = .init("contacts") 17 | } 18 | 19 | // MARK: - AuthorizeContacts 20 | /// 通讯录授权 21 | public class AuthorizeContacts: NSObject, AuthorizeProtocol, @unchecked Sendable { 22 | public static let shared = AuthorizeContacts() 23 | 24 | public func authorizeStatus() -> AuthorizeStatus { 25 | let status = CNContactStore.authorizationStatus(for: .contacts) 26 | switch status { 27 | case .restricted: 28 | return .restricted 29 | case .denied: 30 | return .denied 31 | case .authorized: 32 | return .authorized 33 | default: 34 | return .notDetermined 35 | } 36 | } 37 | 38 | public func requestAuthorize(_ completion: (@MainActor @Sendable (AuthorizeStatus, Error?) -> Void)?) { 39 | CNContactStore().requestAccess(for: .contacts) { granted, error in 40 | let status: AuthorizeStatus = granted ? .authorized : .denied 41 | if completion != nil { 42 | DispatchQueue.fw.mainAsync { 43 | completion?(status, error) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | // MARK: - Autoloader+Contacts 51 | @objc extension Autoloader { 52 | static func loadPlugin_Contacts() { 53 | AuthorizeManager.presetAuthorize(.contacts) { AuthorizeContacts.shared } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Authorize/Microphone/AuthorizeMicrophone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizeMicrophone.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import AVFoundation 9 | #if FWMacroSPM 10 | @_spi(FW) import FWFramework 11 | #endif 12 | 13 | // MARK: - AuthorizeType+Microphone 14 | extension AuthorizeType { 15 | /// 麦克风,Info.plist需配置NSMicrophoneUsageDescription 16 | public static let microphone: AuthorizeType = .init("microphone") 17 | } 18 | 19 | // MARK: - AuthorizeMicrophone 20 | /// 麦克风授权 21 | public class AuthorizeMicrophone: NSObject, AuthorizeProtocol, @unchecked Sendable { 22 | public static let shared = AuthorizeMicrophone() 23 | 24 | public func authorizeStatus() -> AuthorizeStatus { 25 | let status = AVAudioSession.sharedInstance().recordPermission 26 | switch status { 27 | case .denied: 28 | return .denied 29 | case .granted: 30 | return .authorized 31 | default: 32 | return .notDetermined 33 | } 34 | } 35 | 36 | public func requestAuthorize(_ completion: (@MainActor @Sendable (AuthorizeStatus, Error?) -> Void)?) { 37 | AVAudioSession.sharedInstance().requestRecordPermission { granted in 38 | let status: AuthorizeStatus = granted ? .authorized : .denied 39 | if completion != nil { 40 | DispatchQueue.fw.mainAsync { 41 | completion?(status, nil) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | // MARK: - Autoloader+Microphone 49 | @objc extension Autoloader { 50 | static func loadPlugin_Microphone() { 51 | AuthorizeManager.presetAuthorize(.microphone) { AuthorizeMicrophone.shared } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Authorize/Tracking/AuthorizeTracking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizeTracking.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import AdSupport 9 | import AppTrackingTransparency 10 | import UIKit 11 | #if FWMacroSPM 12 | @_spi(FW) import FWFramework 13 | #endif 14 | 15 | // MARK: - Wrapper+UIDevice 16 | @MainActor extension Wrapper where Base: UIDevice { 17 | /// 获取设备IDFA(外部使用),重置广告或系统后会改变,需先检测广告追踪权限 18 | public nonisolated static var deviceIDFA: String { 19 | ASIdentifierManager.shared().advertisingIdentifier.uuidString 20 | } 21 | } 22 | 23 | // MARK: - AuthorizeType+Tracking 24 | extension AuthorizeType { 25 | /// 广告跟踪,Info.plist需配置NSUserTrackingUsageDescription 26 | public static let tracking: AuthorizeType = .init("tracking") 27 | } 28 | 29 | // MARK: - AuthorizeTracking 30 | /// IDFA授权,iOS14+使用AppTrackingTransparency,其它使用AdSupport 31 | public class AuthorizeTracking: NSObject, AuthorizeProtocol, @unchecked Sendable { 32 | public static let shared = AuthorizeTracking() 33 | 34 | public func authorizeStatus() -> AuthorizeStatus { 35 | if #available(iOS 14.0, *) { 36 | let status = ATTrackingManager.trackingAuthorizationStatus 37 | switch status { 38 | case .restricted: 39 | return .restricted 40 | case .denied: 41 | return .denied 42 | case .authorized: 43 | return .authorized 44 | default: 45 | return .notDetermined 46 | } 47 | } else { 48 | return ASIdentifierManager.shared().isAdvertisingTrackingEnabled ? .authorized : .denied 49 | } 50 | } 51 | 52 | public func requestAuthorize(_ completion: (@MainActor @Sendable (AuthorizeStatus, Error?) -> Void)?) { 53 | if #available(iOS 14.0, *) { 54 | ATTrackingManager.requestTrackingAuthorization { _ in 55 | if completion != nil { 56 | DispatchQueue.fw.mainAsync { 57 | completion?(self.authorizeStatus(), nil) 58 | } 59 | } 60 | } 61 | } else { 62 | if completion != nil { 63 | DispatchQueue.fw.mainAsync { 64 | completion?(self.authorizeStatus(), nil) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | // MARK: - Autoloader+Tracking 72 | @objc extension Autoloader { 73 | static func loadPlugin_Tracking() { 74 | AuthorizeManager.presetAuthorize(.tracking) { AuthorizeTracking.shared } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Macros/FWMacroMacros/FWMacroMacros.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct FWMacroMacros: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | MappedValueMacro.self, 8 | PropertyWrapperMacro.self, 9 | SmartSubclassMacro.self 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Macros/FWMacroMacros/MappedValueMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxMacros 4 | 5 | public struct MappedValueMacro: MemberAttributeMacro { 6 | public static func expansion( 7 | of node: AttributeSyntax, 8 | attachedTo declaration: some DeclGroupSyntax, 9 | providingAttributesFor member: some DeclSyntaxProtocol, 10 | in context: some MacroExpansionContext 11 | ) throws -> [AttributeSyntax] { 12 | try PropertyWrapperMacro.expansion(propertyWrapper: "@MappedValue", of: node, attachedTo: declaration, providingAttributesFor: member, in: context) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Macros/FWMacroMacros/PropertyWrapperMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct PropertyWrapperMacro: MemberAttributeMacro { 5 | public static func expansion( 6 | of node: AttributeSyntax, 7 | attachedTo declaration: some DeclGroupSyntax, 8 | providingAttributesFor member: some DeclSyntaxProtocol, 9 | in context: some MacroExpansionContext 10 | ) throws -> [AttributeSyntax] { 11 | let exprs = node.arguments? 12 | .as(LabeledExprListSyntax.self)?.map(\.expression) ?? [] 13 | var attributes: [AttributeSyntax] = [] 14 | for expr in exprs { 15 | let propertyWrapper = expr.trimmed.description 16 | .trimmingCharacters(in: .init(charactersIn: "@\"")) 17 | .replacingOccurrences(of: "\\\"", with: "\"") 18 | try attributes.append(contentsOf: expansion(propertyWrapper: "@" + propertyWrapper, of: node, attachedTo: declaration, providingAttributesFor: member, in: context)) 19 | } 20 | return attributes 21 | } 22 | } 23 | 24 | extension PropertyWrapperMacro { 25 | private struct MacroError: CustomStringConvertible, Error { 26 | let text: String 27 | 28 | init(_ text: String) { 29 | self.text = text 30 | } 31 | 32 | var description: String { 33 | text 34 | } 35 | } 36 | 37 | static func expansion( 38 | propertyWrapper: String, 39 | of node: AttributeSyntax, 40 | attachedTo declaration: some DeclGroupSyntax, 41 | providingAttributesFor member: some DeclSyntaxProtocol, 42 | in context: some MacroExpansionContext 43 | ) throws -> [AttributeSyntax] { 44 | guard declaration.is(StructDeclSyntax.self) || 45 | declaration.is(ClassDeclSyntax.self) else { 46 | throw MacroError("use @\(node.attributeName.description) in `struct` or `class`") 47 | } 48 | 49 | guard let variable = member.as(VariableDeclSyntax.self), 50 | isStoredProperty(variable), 51 | !isIgnoredProperty(variable), 52 | !variable.description.contains(propertyWrapper) else { 53 | return [] 54 | } 55 | 56 | return [AttributeSyntax(stringLiteral: propertyWrapper)] 57 | } 58 | 59 | private static func isStoredProperty(_ syntax: VariableDeclSyntax) -> Bool { 60 | if syntax.modifiers.compactMap({ $0.as(DeclModifierSyntax.self) }).contains(where: { $0.name.text == "static" }) { 61 | return false 62 | } 63 | 64 | if syntax.bindings.count < 1 { 65 | return false 66 | } 67 | 68 | let binding = syntax.bindings.last! 69 | switch binding.accessorBlock?.accessors { 70 | case .none: 71 | return true 72 | case let .accessors(o): 73 | for accessor in o { 74 | switch accessor.accessorSpecifier.tokenKind { 75 | case .keyword(.willSet), .keyword(.didSet): 76 | break 77 | default: 78 | return false 79 | } 80 | } 81 | return true 82 | case .getter: 83 | return false 84 | } 85 | } 86 | 87 | private static func isIgnoredProperty(_ syntax: VariableDeclSyntax) -> Bool { 88 | let patterns = syntax.bindings.map(\.pattern) 89 | let names = patterns.compactMap { $0.as(IdentifierPatternSyntax.self)?.identifier.text } 90 | let ignored = names.firstIndex(where: { $0.hasPrefix("_") || $0.hasSuffix("_") }) != nil 91 | return ignored 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Macros/FWPluginMacros/FWPluginMacros.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if FWMacroSPM 3 | import FWFramework 4 | #endif 5 | 6 | // MARK: - FWPluginMacros 7 | /// MappedValue宏,仅支持class或struct 8 | /// 9 | /// 使用方法: 10 | /// 1. 标记class或struct为自动映射存储属性宏,使用方式:@MappedValueMacro 11 | /// 2. 可自定义字段映射规则,使用方式:@MappedValue("name1", "name2") 12 | /// 3. 以下划线开头或结尾的字段将自动忽略,也可代码忽略:@MappedValue(ignored: true) 13 | @attached(memberAttribute) 14 | public macro MappedValueMacro() = #externalMacro(module: "FWMacroMacros", type: "MappedValueMacro") 15 | 16 | /// 通用PropertyWrapper宏,仅支持class或struct 17 | /// 18 | /// 使用方法: 19 | /// 1. 标记class或struct为属性包装器宏,使用方式:@PropertyWrapperMacro("MappedValue") 20 | /// 2. 如果字段不包含该属性包装器且不以下划线开头或结尾,将自动添加属性包装器 21 | @attached(memberAttribute) 22 | public macro PropertyWrapperMacro(_ name: StaticString...) = #externalMacro(module: "FWMacroMacros", type: "PropertyWrapperMacro") 23 | 24 | /// 继承SmartCodable宏,仅支持class 25 | /// 26 | /// 使用方法: 27 | /// 1. 标记class为属性包装器宏,使用方式:@SmartSubclass 28 | @attached(member, names: named(init(from:)), named(encode(to:)), named(CodingKeys), named(init)) 29 | public macro SmartSubclass() = #externalMacro(module: "FWMacroMacros", type: "SmartSubclassMacro") 30 | 31 | // MARK: - Autoloader+Macros 32 | #if FWPluginMacros 33 | @objc extension Autoloader { 34 | static func loadPlugin_Macros() {} 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /Sources/FWPlugin/Macros/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import CompilerPluginSupport 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FWPluginMacros", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | .library( 14 | name: "FWPluginMacros", 15 | targets: ["FWPluginMacros"] 16 | ) 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0") 20 | ], 21 | targets: [ 22 | .macro( 23 | name: "FWMacroMacros", 24 | dependencies: [ 25 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 26 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 27 | ], 28 | path: "FWMacroMacros" 29 | ), 30 | .target( 31 | name: "FWPluginMacros", 32 | dependencies: ["FWMacroMacros"], 33 | path: "FWPluginMacros" 34 | ) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Module/Controller/HostingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostingController.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | #if FWMacroSPM 11 | @_spi(FW) import FWFramework 12 | #endif 13 | 14 | // MARK: - HostingController 15 | /// SwiftUI控制器包装类,可将View事件用delegate代理到VC,兼容ViewControllerProtocol 16 | /// 17 | /// Controller在MVVM中也为View的角色,可持有ViewModel,负责生命周期和界面跳转 18 | open class HostingController: UIHostingController { 19 | // MARK: - Lifecyecle 20 | public init() { 21 | super.init(rootView: AnyView(EmptyView())) 22 | let isSetup = FrameworkConfiguration.isViewControllerProtocol?(self) ?? false 23 | if !isSetup { 24 | didInitialize() 25 | } 26 | } 27 | 28 | @MainActor public dynamic required init?(coder aDecoder: NSCoder) { 29 | super.init(coder: aDecoder, rootView: AnyView(EmptyView())) 30 | let isSetup = FrameworkConfiguration.isViewControllerProtocol?(self) ?? false 31 | if !isSetup { 32 | didInitialize() 33 | } 34 | } 35 | 36 | override open func viewDidLoad() { 37 | super.viewDidLoad() 38 | let isSetup = FrameworkConfiguration.isViewControllerProtocol?(self) ?? false 39 | if !isSetup { 40 | setupNavbar() 41 | setupSubviews() 42 | setupLayout() 43 | } 44 | } 45 | 46 | // MARK: - Setup 47 | /// 初始化完成,init自动调用,子类重写 48 | open func didInitialize() {} 49 | 50 | /// 初始化导航栏,viewDidLoad自动调用,子类重写 51 | open func setupNavbar() {} 52 | 53 | /// 初始化子视图,viewDidLoad自动调用,子类重写,可结合StateView实现状态机 54 | open func setupSubviews() {} 55 | 56 | /// 初始化布局,viewDidLoad自动调用,子类重写 57 | open func setupLayout() {} 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Module/Controller/StateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateView.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | #if FWMacroSPM 11 | @_spi(FW) import FWFramework 12 | #endif 13 | 14 | // MARK: - StateView 15 | /// SwiftUI状态视图 16 | /// 17 | /// \@State: 内部值传递,赋值时会触发View刷新 18 | /// \@Binding: 外部引用传递,实现向外传递引用 19 | /// \@ObservableObject: 可被订阅的对象,属性标记@Published时生效 20 | /// \@ObservedObject: View订阅监听,收到通知时刷新View,不被View持有,随时可能被销毁,适合外部数据 21 | /// \@EnvironmentObject: 全局环境对象,使用environmentObject方法绑定,View及其子层级可直接读取 22 | /// \@StateObject: View引用对象,生命周期和View保持一致,刷新时数据会保持直到View被销毁 23 | public struct StateView: View { 24 | @State public var state: ViewLoadingState = .ready 25 | 26 | @ViewBuilder var ready: (Self) -> AnyView 27 | @ViewBuilder var loading: (Self) -> AnyView 28 | @ViewBuilder var content: (Self, Any?) -> AnyView 29 | @ViewBuilder var failure: (Self, Error?) -> AnyView 30 | 31 | public init( 32 | @ViewBuilder content: @escaping (Self, Any?) -> Content 33 | ) { 34 | self.ready = { $0.transition(to: .success()) } 35 | self.loading = { $0.transition(to: .success()) } 36 | self.content = { content($0, $1).eraseToAnyView() } 37 | self.failure = { $0.transition(to: .success($1)) } 38 | } 39 | 40 | public init( 41 | @ViewBuilder loading: @escaping (Self) -> Loading, 42 | @ViewBuilder content: @escaping (Self, Any?) -> Content, 43 | @ViewBuilder failure: @escaping (Self, Error?) -> Failure 44 | ) { 45 | self.ready = { $0.transition(to: .loading) } 46 | self.loading = { loading($0).eraseToAnyView() } 47 | self.content = { content($0, $1).eraseToAnyView() } 48 | self.failure = { failure($0, $1).eraseToAnyView() } 49 | } 50 | 51 | public init( 52 | @ViewBuilder ready: @escaping (Self) -> Ready, 53 | @ViewBuilder loading: @escaping (Self) -> Loading, 54 | @ViewBuilder content: @escaping (Self, Any?) -> Content, 55 | @ViewBuilder failure: @escaping (Self, Error?) -> Failure 56 | ) { 57 | self.ready = { ready($0).eraseToAnyView() } 58 | self.loading = { loading($0).eraseToAnyView() } 59 | self.content = { content($0, $1).eraseToAnyView() } 60 | self.failure = { failure($0, $1).eraseToAnyView() } 61 | } 62 | 63 | private func transition(to newState: ViewLoadingState) -> AnyView { 64 | InvisibleView() 65 | .onAppear { state = newState } 66 | .eraseToAnyView() 67 | } 68 | 69 | public var body: some View { 70 | Group { 71 | switch state { 72 | case .ready: 73 | ready(self) 74 | case .loading: 75 | loading(self) 76 | case let .success(object): 77 | content(self, object) 78 | case let .failure(error): 79 | failure(self, error) 80 | } 81 | } 82 | } 83 | } 84 | 85 | // MARK: - InvisibleView 86 | /// 不可见视图,当某个场景EmptyView不生效时可使用InvisibleView替代,比如EmptyView不触发onAppear 87 | public struct InvisibleView: View { 88 | public init() {} 89 | 90 | public var body: some View { 91 | Color.black.opacity(0.0001) 92 | .frame(width: 0, height: 0) 93 | .allowsHitTesting(false) 94 | .accessibility(hidden: true) 95 | } 96 | } 97 | 98 | #endif 99 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Module/View/ImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageView.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | #if FWMacroSPM 11 | @_spi(FW) import FWFramework 12 | @_spi(FW) import FWUIKit 13 | #endif 14 | 15 | // MARK: - ImageView 16 | /// 图片视图,支持网络图片和动图 17 | public struct ImageView: UIViewRepresentable { 18 | var url: URLParameter? 19 | var placeholder: UIImage? 20 | var options: WebImageOptions = [] 21 | var context: [ImageCoderOptions: Any]? 22 | var completion: (@MainActor @Sendable (UIImageView, UIImage?) -> Void)? 23 | var contentMode: UIView.ContentMode = .scaleAspectFill 24 | 25 | /// 指定本地占位图片初始化 26 | public init(_ placeholder: UIImage? = nil) { 27 | self.placeholder = placeholder 28 | } 29 | 30 | /// 指定网络图片URL初始化 31 | public init(url: URLParameter?) { 32 | self.url = url 33 | } 34 | 35 | /// 设置网络图片URL 36 | public func url(_ url: URLParameter?) -> Self { 37 | var result = self 38 | result.url = url 39 | return result 40 | } 41 | 42 | /// 设置网络图片加载选项 43 | public func options(_ options: WebImageOptions) -> Self { 44 | var result = self 45 | result.options = options 46 | return result 47 | } 48 | 49 | /// 设置本地占位图片 50 | public func placeholder(_ placeholder: UIImage?) -> Self { 51 | var result = self 52 | result.placeholder = placeholder 53 | return result 54 | } 55 | 56 | /// 设置图片显示内容模式,默认scaleAspectFill 57 | public func contentMode(_ contentMode: UIView.ContentMode) -> Self { 58 | var result = self 59 | result.contentMode = contentMode 60 | return result 61 | } 62 | 63 | // MARK: - UIViewRepresentable 64 | public typealias UIViewType = ResizableView 65 | 66 | public func makeUIView(context: Context) -> ResizableView { 67 | let imageView = UIImageView.fw.animatedImageView() 68 | let uiView = ResizableView(imageView) 69 | uiView.content.contentMode = contentMode 70 | uiView.content.fw.setImage(url: url, placeholderImage: placeholder, options: options, context: self.context, completion: completion != nil ? { @MainActor @Sendable image, _ in completion?(imageView, image) } : nil) 71 | return uiView 72 | } 73 | 74 | public func updateUIView(_ uiView: ResizableView, context: Context) { 75 | uiView.content.contentMode = contentMode 76 | } 77 | 78 | public static func dismantleUIView(_ uiView: ResizableView, coordinator: ()) { 79 | uiView.content.fw.cancelImageRequest() 80 | } 81 | } 82 | 83 | // MARK: - ResizableView 84 | /// 可调整大小的视图包装器,解决frame尺寸变为图片尺寸等问题 85 | public class ResizableView: UIView { 86 | public var content: Content 87 | public var resizable = true 88 | 89 | public init(_ content: Content, frame: CGRect = .zero) { 90 | self.content = content 91 | super.init(frame: frame) 92 | addSubview(content) 93 | } 94 | 95 | required init?(coder: NSCoder) { 96 | fatalError("init(coder:) has not been implemented") 97 | } 98 | 99 | override public func layoutSubviews() { 100 | super.layoutSubviews() 101 | content.frame = bounds 102 | } 103 | 104 | override public var frame: CGRect { 105 | didSet { invalidateIntrinsicContentSize() } 106 | } 107 | 108 | override public var bounds: CGRect { 109 | didSet { invalidateIntrinsicContentSize() } 110 | } 111 | 112 | override public var intrinsicContentSize: CGSize { 113 | resizable ? super.intrinsicContentSize : content.intrinsicContentSize 114 | } 115 | 116 | override public func sizeThatFits(_ size: CGSize) -> CGSize { 117 | resizable ? super.intrinsicContentSize : content.intrinsicContentSize 118 | } 119 | } 120 | 121 | #endif 122 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Plugin/ViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBuilder.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | #if FWMacroSPM 11 | @_spi(FW) import FWFramework 12 | #endif 13 | 14 | // MARK: - Text+ViewBuilder 15 | extension Text { 16 | /// 拼接行内多文本 17 | public static func concatenate( 18 | @ArrayResultBuilder _ items: () -> [Text] 19 | ) -> Self { 20 | items().reduce(Text(""), +) 21 | } 22 | 23 | /// 初始化并拼接行内多文本 24 | public init( 25 | @ArrayResultBuilder _ items: () -> [Text] 26 | ) { 27 | self = items().reduce(Text(""), +) 28 | } 29 | 30 | /// 拼接行内多文本 31 | public func concatenate( 32 | @ArrayResultBuilder _ items: () -> [Text] 33 | ) -> Self { 34 | items().reduce(self, +) 35 | } 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Plugin/ViewContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewContext.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import Combine 10 | import SwiftUI 11 | 12 | /// 视图上下文 13 | public class ViewContext: ObservableObject { 14 | // MARK: - ViewController 15 | /// 当前视图控制器 16 | public weak var viewController: UIViewController? 17 | 18 | /// 获取当前导航栏控制器 19 | @MainActor public weak var navigationController: UINavigationController? { 20 | viewController?.navigationController 21 | } 22 | 23 | /// 获取当前UIView根视图 24 | @MainActor public weak var hostingView: UIView? { 25 | viewController?.view 26 | } 27 | 28 | /// 获取当前AnyView根视图 29 | @MainActor public var rootView: AnyView? { 30 | if let hostingController = viewController as? UIHostingController { 31 | return hostingController.rootView 32 | } 33 | return nil 34 | } 35 | 36 | // MARK: - Object 37 | /// 自定义对象,自动广播,订阅方式:onReceive(viewContext.$object) 38 | @Published public var object: Any? 39 | 40 | // MARK: - UserInfo 41 | /// 自定义用户信息,可初始化时设置,也可修改后手动广播 42 | public var userInfo: [AnyHashable: Any]? 43 | 44 | // MARK: - Subject 45 | /// 上下文Subject,可订阅,需手工触发send发送广播 46 | public let subject = PassthroughSubject() 47 | 48 | /// 手动发送广播,一般修改userInfo后调用 49 | public func send() { 50 | subject.send(self) 51 | } 52 | 53 | // MARK: - Lifecycle 54 | /// 初始化方法,可指定视图控制器、自定义对象和用户信息 55 | public init(_ viewController: UIViewController?, object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) { 56 | self.viewController = viewController 57 | self.object = object 58 | self.userInfo = userInfo 59 | } 60 | } 61 | 62 | extension EnvironmentValues { 63 | /// 视图上下文Key 64 | private struct ViewContextKey: EnvironmentKey { 65 | static var defaultValue: ViewContext { 66 | ViewContext(nil) 67 | } 68 | } 69 | 70 | /// 访问视图上下文 71 | public var viewContext: ViewContext { 72 | get { self[ViewContextKey.self] } 73 | set { self[ViewContextKey.self] = newValue } 74 | } 75 | } 76 | 77 | @MainActor extension View { 78 | /// 设置视图上下文,可指定自定义对象 79 | public func viewContext(_ viewController: UIViewController?, object: Any? = nil) -> some View { 80 | environment(\.viewContext, ViewContext(viewController, object: object)) 81 | } 82 | 83 | /// 设置视图上下文,可指定自定义对象和用户信息 84 | public func viewContext(_ viewController: UIViewController?, object: Any? = nil, userInfo: [AnyHashable: Any]?) -> some View { 85 | environment(\.viewContext, ViewContext(viewController, object: object, userInfo: userInfo)) 86 | } 87 | 88 | /// 转换视图上下文,内部可使用DispatchQueue.main.async执行异步方法 89 | /// 90 | /// 如果要监听上下文变化,可使用如下方式: 91 | /// 1. onReceive(viewContext.subject) 92 | /// 2. onReceive(viewContext.$object) 93 | /// 3. viewContext.$object.receive(on: RunLoop.main) 94 | public func transformViewContext(transform: @escaping (ViewContext) -> Void) -> some View { 95 | transformEnvironment(\.viewContext) { viewContext in 96 | transform(viewContext) 97 | } 98 | } 99 | 100 | /// 快速包装视图到上下文控制器 101 | public func wrappedContextController() -> UIHostingController { 102 | let hostingController = UIHostingController(rootView: AnyView(EmptyView())) 103 | hostingController.rootView = AnyView(viewContext(hostingController)) 104 | return hostingController 105 | } 106 | } 107 | 108 | extension UIHostingController where Content == AnyView { 109 | /// 快速创建视图上下文控制器 110 | public static func contextController(@ViewBuilder content: () -> T) -> UIHostingController { 111 | let hostingController = UIHostingController(rootView: AnyView(EmptyView())) 112 | hostingController.rootView = AnyView(content().viewContext(hostingController)) 113 | return hostingController 114 | } 115 | } 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Plugin/ViewPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPreference.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | 11 | // MARK: - ViewPreferenceKey 12 | /// 通用视图配置Key类 13 | open class ViewPreferenceKey: PreferenceKey { 14 | public typealias Value = T? 15 | 16 | public static var defaultValue: Value { 17 | nil 18 | } 19 | 20 | public static func reduce(value: inout Value, nextValue: () -> Value) { 21 | let newValue = nextValue() ?? value 22 | if value != newValue { 23 | value = newValue 24 | } 25 | } 26 | } 27 | 28 | // MARK: - ViewSizePreferenceKey 29 | private final class ViewSizePreferenceKey: ViewPreferenceKey {} 30 | 31 | extension View { 32 | /// 捕获当前视图大小 33 | public func captureSize(in binding: Binding) -> some View { 34 | overlay( 35 | GeometryReader { proxy in 36 | Color.clear.preference( 37 | key: ViewSizePreferenceKey.self, 38 | value: proxy.size 39 | ) 40 | .onAppear { 41 | if binding.wrappedValue != proxy.size { 42 | binding.wrappedValue = proxy.size 43 | } 44 | } 45 | } 46 | ) 47 | .onPreferenceChange(ViewSizePreferenceKey.self) { size in 48 | if let size, binding.wrappedValue != size { 49 | binding.wrappedValue = size 50 | } 51 | } 52 | .preference(key: ViewSizePreferenceKey.self, value: nil) 53 | } 54 | } 55 | 56 | // MARK: - ViewContentOffsetPreferenceKey 57 | private final class ViewContentOffsetPreferenceKey: ViewPreferenceKey {} 58 | 59 | extension View { 60 | /// 捕获当前滚动视图内容偏移,需滚动视图调用,且用GeometryReader包裹滚动视图 61 | /// 62 | /// 使用示例: 63 | /// GeometryReader { proxy in 64 | /// List { ... } 65 | /// .captureContentOffset(in: $contentOffsets) 66 | /// } 67 | public func captureContentOffset(in binding: Binding) -> some View { 68 | onPreferenceChange(ViewContentOffsetPreferenceKey.self, perform: { value in 69 | binding.wrappedValue = value ?? .zero 70 | }) 71 | } 72 | 73 | /// 捕获当前滚动视图内容偏移,需滚动视图第一个子视图调用 74 | /// 75 | /// 使用示例: 76 | /// GeometryReader { proxy in 77 | /// List { 78 | /// Cell 79 | /// .captureContentOffset(proxy: proxy) 80 | /// 81 | /// ... 82 | /// } 83 | /// .captureContentOffset(in: $contentOffsets) 84 | /// } 85 | public func captureContentOffset(proxy outsideProxy: GeometryProxy) -> some View { 86 | let outsideFrame = outsideProxy.frame(in: .global) 87 | 88 | return ZStack { 89 | GeometryReader { insideProxy in 90 | Color.clear.preference( 91 | key: ViewContentOffsetPreferenceKey.self, 92 | value: CGPoint( 93 | x: outsideFrame.minX - insideProxy.frame(in: .global).minX, 94 | y: outsideFrame.minY - insideProxy.frame(in: .global).minY 95 | ) 96 | ) 97 | .frame(width: 0, height: 0) 98 | } 99 | self 100 | } 101 | } 102 | 103 | /// 监听当前滚动视图内容偏移实现悬停效果,需GeometryReader调用 104 | /// 105 | /// 使用示例: 106 | /// GeometryReader { proxy in 107 | /// List { 108 | /// Cell 109 | /// .captureContentOffset(proxy: proxy) 110 | /// 111 | /// ... 112 | /// } 113 | /// .captureContentOffset(in: $contentOffsets) 114 | /// } 115 | /// .hoverContentOffset(visible: contentOffset.y >= offset) { 116 | /// ... 117 | /// } 118 | public func hoverContentOffset( 119 | alignment: Alignment = .top, 120 | visible: Bool = true, 121 | @ViewBuilder content: () -> Content 122 | ) -> some View { 123 | ZStack(alignment: alignment) { 124 | self 125 | 126 | content() 127 | .hidden(!visible) 128 | } 129 | } 130 | } 131 | 132 | #endif 133 | -------------------------------------------------------------------------------- /Sources/FWSwiftUI/Plugin/ViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewWrapper.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | 11 | // MARK: - ViewWrapper 12 | /// 通用UIView包装器 13 | public struct ViewWrapper: UIViewRepresentable { 14 | var maker: (@MainActor @Sendable () -> T)? 15 | var updater: (@MainActor @Sendable (T) -> Void)? 16 | 17 | /// 指定makeUIView闭包初始化 18 | public init(_ maker: (@MainActor @Sendable () -> T)? = nil) { 19 | self.maker = maker 20 | } 21 | 22 | /// 指定updateUIView闭包初始化 23 | public init(updater: @escaping @MainActor @Sendable (T) -> Void) { 24 | self.updater = updater 25 | } 26 | 27 | /// 指定makeUIView闭包和updateUIView闭包初始化 28 | public init(_ maker: @escaping @MainActor @Sendable () -> T, updater: @escaping @MainActor @Sendable (T) -> Void) { 29 | self.maker = maker 30 | self.updater = updater 31 | } 32 | 33 | /// 设置makeUIView闭包 34 | public func maker(_ maker: @escaping @MainActor @Sendable () -> T) -> ViewWrapper { 35 | var result = self 36 | result.maker = maker 37 | return result 38 | } 39 | 40 | /// 设置updateUIView闭包 41 | public func updater(_ updater: @escaping @MainActor @Sendable (T) -> Void) -> ViewWrapper { 42 | var result = self 43 | result.updater = updater 44 | return result 45 | } 46 | 47 | // MARK: - UIViewRepresentable 48 | 49 | public typealias UIViewType = T 50 | 51 | public func makeUIView(context: Context) -> T { 52 | maker?() ?? T() 53 | } 54 | 55 | public func updateUIView(_ uiView: T, context: Context) { 56 | updater?(uiView) 57 | } 58 | } 59 | 60 | // MARK: - ViewControllerWrapper 61 | /// 通用UIViewController包装器 62 | public struct ViewControllerWrapper: UIViewControllerRepresentable { 63 | var maker: (@MainActor @Sendable () -> T)? 64 | var updater: (@MainActor @Sendable (T) -> Void)? 65 | 66 | /// 指定makeUIViewController闭包初始化 67 | public init(_ maker: (@MainActor @Sendable () -> T)? = nil) { 68 | self.maker = maker 69 | } 70 | 71 | /// 指定updateUIViewController闭包初始化 72 | public init(updater: @escaping @MainActor @Sendable (T) -> Void) { 73 | self.updater = updater 74 | } 75 | 76 | /// 指定makeUIViewController闭包和updateUIViewController闭包初始化 77 | public init(_ maker: @escaping @MainActor @Sendable () -> T, updater: @escaping @MainActor @Sendable (T) -> Void) { 78 | self.maker = maker 79 | self.updater = updater 80 | } 81 | 82 | /// 设置makeUIViewController闭包 83 | public func maker(_ maker: @escaping @MainActor @Sendable () -> T) -> ViewControllerWrapper { 84 | var result = self 85 | result.maker = maker 86 | return result 87 | } 88 | 89 | /// 设置updateUIViewController闭包 90 | public func updater(_ updater: @escaping @MainActor @Sendable (T) -> Void) -> ViewControllerWrapper { 91 | var result = self 92 | result.updater = updater 93 | return result 94 | } 95 | 96 | // MARK: - UIViewControllerRepresentable 97 | 98 | public typealias UIViewControllerType = T 99 | 100 | public func makeUIViewController(context: Context) -> T { 101 | maker?() ?? T() 102 | } 103 | 104 | public func updateUIViewController(_ uiViewController: T, context: Context) { 105 | updater?(uiViewController) 106 | } 107 | } 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Extend/Service/Recognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recognizer.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/25. 6 | // 7 | 8 | import NaturalLanguage 9 | import UIKit 10 | #if canImport(Vision) 11 | import Vision 12 | #endif 13 | 14 | /// 图像文本识别器 15 | public class Recognizer { 16 | /// 识别结果 17 | public struct Result: Sendable { 18 | /// 识别文本 19 | public var text: String = "" 20 | /// 可信度,0到1 21 | public var confidence: Float = 0 22 | /// 图片大小 23 | public var imageSize: CGSize = .zero 24 | /// 识别区域 25 | public var rect: CGRect = .zero 26 | 27 | public init() {} 28 | } 29 | 30 | /// 识别图片文字,可设置语言(zh-CN,en-US)等,完成时主线程回调结果 31 | public static func recognizeText(in image: CGImage, configuration: (@Sendable (VNRecognizeTextRequest) -> Void)?, completion: @escaping @MainActor @Sendable ([Result]) -> Void) { 32 | DispatchQueue.global(qos: .userInitiated).async { 33 | performOcr(image: image, configuration: configuration) { results in 34 | DispatchQueue.main.async { 35 | completion(results) 36 | } 37 | } 38 | } 39 | } 40 | 41 | /// 识别指定文字的主要语言,可指定限制范围 42 | public static func recognizeLanguage(with string: String, constraints: [NLLanguage]? = nil) -> NLLanguage? { 43 | let recognizer = NLLanguageRecognizer() 44 | if let constraints { recognizer.languageConstraints = constraints } 45 | recognizer.processString(string.replacingOccurrences(of: "\n", with: "")) 46 | return recognizer.dominantLanguage 47 | } 48 | 49 | // MARK: - Private 50 | private static func performOcr(image: CGImage, configuration: (@Sendable (VNRecognizeTextRequest) -> Void)?, completion: @escaping @Sendable ([Result]) -> Void) { 51 | let textRequest = VNRecognizeTextRequest { request, _ in 52 | let imageSize = CGSize(width: image.width, height: image.height) 53 | guard let results = request.results as? [VNRecognizedTextObservation], !results.isEmpty else { 54 | completion([]) 55 | return 56 | } 57 | 58 | let outputObjects: [Result] = results.compactMap { result in 59 | guard let candidate = result.topCandidates(1).first, 60 | let box = try? candidate.boundingBox(for: candidate.string.startIndex.. CGRect { 91 | let topLeft = VNImagePointForNormalizedPoint(boundingBox.topLeft, 92 | Int(imageSize.width), 93 | Int(imageSize.height)) 94 | let bottomRight = VNImagePointForNormalizedPoint(boundingBox.bottomRight, 95 | Int(imageSize.width), 96 | Int(imageSize.height)) 97 | return CGRect(x: topLeft.x, y: imageSize.height - topLeft.y, 98 | width: abs(bottomRight.x - topLeft.x), 99 | height: abs(topLeft.y - bottomRight.y)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Module/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | import UIKit 9 | 10 | /// SceneDelegate基类 11 | open class SceneResponder: UIResponder, UIWindowSceneDelegate { 12 | /// 场景主window 13 | open var window: UIWindow? 14 | 15 | /// 初始化根控制器,子类重写 16 | open func setupController() { 17 | /* 18 | window?.rootViewController = TabBarController() 19 | */ 20 | } 21 | 22 | // MARK: - UIWindowSceneDelegate 23 | open func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 24 | if let windowScene = scene as? UIWindowScene { 25 | window = UIWindow(windowScene: windowScene) 26 | window?.makeKeyAndVisible() 27 | setupController() 28 | } 29 | } 30 | 31 | open func sceneDidDisconnect(_ scene: UIScene) {} 32 | 33 | open func sceneDidBecomeActive(_ scene: UIScene) {} 34 | 35 | open func sceneWillResignActive(_ scene: UIScene) {} 36 | 37 | open func sceneWillEnterForeground(_ scene: UIScene) {} 38 | 39 | open func sceneDidEnterBackground(_ scene: UIScene) {} 40 | } 41 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Module/Controller/ScrollViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewController.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | import UIKit 9 | #if FWMacroSPM 10 | @_spi(FW) import FWFramework 11 | #endif 12 | 13 | // MARK: - ScrollViewControllerProtocol 14 | /// 滚动视图控制器协议,可覆写 15 | @MainActor public protocol ScrollViewControllerProtocol: ViewControllerProtocol { 16 | /// 滚动视图,默认不显示滚动条 17 | var scrollView: UIScrollView { get } 18 | 19 | /// 内容容器视图,自动撑开,子视图需要添加到此视图上 20 | var contentView: UIView { get } 21 | 22 | /// 渲染滚动视图,setupSubviews之前调用,默认空实现 23 | func setupScrollView() 24 | 25 | /// 渲染滚动视图布局,setupSubviews之前调用,默认铺满 26 | func setupScrollLayout() 27 | } 28 | 29 | extension ScrollViewControllerProtocol where Self: UIViewController { 30 | /// 滚动视图,默认不显示滚动条 31 | public var scrollView: UIScrollView { 32 | if let result = fw.property(forName: "scrollView") as? UIScrollView { 33 | return result 34 | } else { 35 | let result = UIScrollView() 36 | result.showsVerticalScrollIndicator = false 37 | result.showsHorizontalScrollIndicator = false 38 | fw.setProperty(result, forName: "scrollView") 39 | return result 40 | } 41 | } 42 | 43 | /// 内容容器视图,自动撑开,子视图需要添加到此视图上 44 | public var contentView: UIView { 45 | if let result = fw.property(forName: "contentView") as? UIView { 46 | return result 47 | } else { 48 | let result = UIView() 49 | fw.setProperty(result, forName: "contentView") 50 | return result 51 | } 52 | } 53 | 54 | /// 渲染滚动视图,setupSubviews之前调用,默认空实现 55 | public func setupScrollView() {} 56 | 57 | /// 渲染滚动视图布局,setupSubviews之前调用,默认铺满 58 | public func setupScrollLayout() { 59 | scrollView.fw.pinEdges(autoScale: false) 60 | } 61 | } 62 | 63 | // MARK: - ViewControllerManager+ScrollViewControllerProtocol 64 | extension ViewControllerManager { 65 | @MainActor func scrollViewControllerViewDidLoad(_ viewController: UIViewController) { 66 | guard let viewController = viewController as? UIViewController & ScrollViewControllerProtocol else { return } 67 | 68 | let scrollView = viewController.scrollView 69 | if let popupController = viewController as? PopupViewControllerProtocol { 70 | popupController.popupView.addSubview(scrollView) 71 | } else { 72 | viewController.view.addSubview(scrollView) 73 | } 74 | 75 | let contentView = viewController.contentView 76 | scrollView.addSubview(contentView) 77 | contentView.fw.pinEdges(autoScale: false) 78 | 79 | hookScrollViewController?(viewController) 80 | 81 | viewController.setupScrollView() 82 | viewController.setupScrollLayout() 83 | scrollView.setNeedsLayout() 84 | scrollView.layoutIfNeeded() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Module/Controller/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | import UIKit 9 | #if FWMacroSPM 10 | @_spi(FW) import FWFramework 11 | #endif 12 | 13 | // MARK: - TableDelegateControllerProtocol 14 | /// 表格代理控制器协议,数据源和事件代理为tableDelegate,可覆写 15 | @MainActor public protocol TableDelegateControllerProtocol: ViewControllerProtocol, UITableViewDelegate { 16 | /// 关联表格数据元素类型,默认Any 17 | associatedtype TableElement = Any 18 | 19 | /// 表格视图,默认不显示滚动条,Footer为空视图。Plain有悬停,Group无悬停 20 | var tableView: UITableView { get } 21 | 22 | /// 表格代理,同表格tableDelegate,延迟加载 23 | var tableDelegate: TableViewDelegate { get } 24 | 25 | /// 表格数据,默认空数组,延迟加载 26 | var tableData: [TableElement] { get set } 27 | 28 | /// 渲染表格视图样式,默认Plain 29 | func setupTableStyle() -> UITableView.Style 30 | 31 | /// 渲染表格视图,setupSubviews之前调用,默认空实现 32 | func setupTableView() 33 | 34 | /// 渲染表格视图布局,setupSubviews之前调用,默认铺满 35 | func setupTableLayout() 36 | } 37 | 38 | // MARK: - TableViewControllerProtocol 39 | /// 表格视图控制器协议,数据源和事件代理为控制器,可覆写 40 | @MainActor public protocol TableViewControllerProtocol: TableDelegateControllerProtocol, UITableViewDataSource {} 41 | 42 | // MARK: - UIViewController+TableViewControllerProtocol 43 | extension TableDelegateControllerProtocol where Self: UIViewController { 44 | /// 表格视图,默认不显示滚动条,Footer为空视图。Plain有悬停,Group无悬停 45 | public var tableView: UITableView { 46 | if let result = fw.property(forName: "tableView") as? UITableView { 47 | return result 48 | } else { 49 | let result = UITableView.fw.tableView(setupTableStyle()) 50 | fw.setProperty(result, forName: "tableView") 51 | return result 52 | } 53 | } 54 | 55 | /// 表格代理,同表格tableDelegate,延迟加载 56 | public var tableDelegate: TableViewDelegate { 57 | tableView.fw.tableDelegate 58 | } 59 | 60 | /// 表格数据,默认空数组,延迟加载 61 | public var tableData: [TableElement] { 62 | get { fw.property(forName: "tableData") as? [TableElement] ?? [] } 63 | set { fw.setProperty(newValue, forName: "tableData") } 64 | } 65 | 66 | /// 渲染表格视图样式,默认Plain 67 | public func setupTableStyle() -> UITableView.Style { 68 | .plain 69 | } 70 | 71 | /// 渲染表格视图,setupSubviews之前调用,默认空实现 72 | public func setupTableView() {} 73 | 74 | /// 渲染表格视图布局,setupSubviews之前调用,默认铺满 75 | public func setupTableLayout() { 76 | tableView.fw.pinEdges(autoScale: false) 77 | } 78 | } 79 | 80 | // MARK: - ViewControllerManager+TableViewControllerProtocol 81 | extension ViewControllerManager { 82 | @MainActor func tableViewControllerViewDidLoad(_ viewController: UIViewController) { 83 | guard let viewController = viewController as? any UIViewController & TableDelegateControllerProtocol else { return } 84 | 85 | let tableView = viewController.tableView 86 | if let viewController = viewController as? any UIViewController & TableViewControllerProtocol { 87 | tableView.dataSource = viewController 88 | tableView.delegate = viewController 89 | } else { 90 | viewController.tableDelegate.delegate = viewController 91 | tableView.dataSource = viewController.tableDelegate 92 | tableView.delegate = viewController.tableDelegate 93 | } 94 | if let popupController = viewController as? PopupViewControllerProtocol { 95 | popupController.popupView.addSubview(tableView) 96 | } else { 97 | viewController.view.addSubview(tableView) 98 | } 99 | 100 | hookTableViewController?(viewController) 101 | 102 | viewController.setupTableView() 103 | viewController.setupTableLayout() 104 | tableView.setNeedsLayout() 105 | tableView.layoutIfNeeded() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Module/View/CornerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerView.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2023/1/4. 6 | // 7 | 8 | import QuartzCore 9 | import UIKit 10 | #if FWMacroSPM 11 | @_spi(FW) import FWFramework 12 | #endif 13 | 14 | // MARK: - RoundedCornerView 15 | /// 半圆圆角View,无需frame快捷设置半圆圆角、边框等 16 | open class RoundedCornerView: UIView { 17 | /// 是否是半圆圆角,默认true 18 | open var isRoundedCorner: Bool = true { 19 | didSet { setNeedsLayout() } 20 | } 21 | 22 | /// 自定义圆角半径,优先级高,默认nil不生效 23 | open var cornerRadius: CGFloat? { 24 | didSet { 25 | if let cornerRadius { 26 | layer.cornerRadius = cornerRadius 27 | } 28 | setNeedsLayout() 29 | } 30 | } 31 | 32 | /// 自定义边框颜色,默认nil不生效 33 | open var borderColor: UIColor? { 34 | didSet { layer.borderColor = borderColor?.cgColor } 35 | } 36 | 37 | /// 自定义边框宽度,默认0 38 | open var borderWidth: CGFloat = 0 { 39 | didSet { layer.borderWidth = borderWidth } 40 | } 41 | 42 | /// 自定义layoutSubviews句柄,默认nil 43 | open var layoutSubviewsBlock: ((UIView) -> Void)? { 44 | didSet { setNeedsLayout() } 45 | } 46 | 47 | /// 快捷初始化带边框半圆圆角 48 | public convenience init(borderColor: UIColor?, borderWidth: CGFloat) { 49 | self.init(frame: .zero) 50 | self.borderColor = borderColor 51 | self.borderWidth = borderWidth 52 | layer.borderColor = borderColor?.cgColor 53 | layer.borderWidth = borderWidth 54 | } 55 | 56 | override public init(frame: CGRect) { 57 | super.init(frame: frame) 58 | layer.masksToBounds = true 59 | } 60 | 61 | public required init?(coder: NSCoder) { 62 | super.init(coder: coder) 63 | layer.masksToBounds = true 64 | } 65 | 66 | override open func layoutSubviews() { 67 | super.layoutSubviews() 68 | if cornerRadius == nil, isRoundedCorner { 69 | layer.cornerRadius = bounds.height / 2.0 70 | } 71 | layoutSubviewsBlock?(self) 72 | } 73 | } 74 | 75 | // MARK: - RectCornerView 76 | /// 不规则圆角View,无需frame快捷设置不规则圆角、边框等 77 | open class RectCornerView: UIView { 78 | /// 自定义圆角位置,默认allCorners 79 | open var rectCorner: UIRectCorner = .allCorners { 80 | didSet { setNeedsLayout() } 81 | } 82 | 83 | /// 自定义圆角半径,默认0 84 | open var cornerRadius: CGFloat = 0 { 85 | didSet { setNeedsLayout() } 86 | } 87 | 88 | /// 自定义边框颜色,默认nil不生效 89 | open var borderColor: UIColor? { 90 | didSet { setNeedsLayout() } 91 | } 92 | 93 | /// 自定义边框宽度,默认0 94 | open var borderWidth: CGFloat = 0 { 95 | didSet { setNeedsLayout() } 96 | } 97 | 98 | /// 自定义layoutSubviews句柄,默认nil 99 | open var layoutSubviewsBlock: ((UIView) -> Void)? { 100 | didSet { setNeedsLayout() } 101 | } 102 | 103 | /// 快捷初始化不规则圆角 104 | public convenience init(rectCorner: UIRectCorner, cornerRadius: CGFloat) { 105 | self.init(frame: .zero) 106 | self.rectCorner = rectCorner 107 | self.cornerRadius = cornerRadius 108 | } 109 | 110 | override public init(frame: CGRect) { 111 | super.init(frame: frame) 112 | layer.masksToBounds = true 113 | } 114 | 115 | public required init?(coder: NSCoder) { 116 | super.init(coder: coder) 117 | layer.masksToBounds = true 118 | } 119 | 120 | override open func layoutSubviews() { 121 | super.layoutSubviews() 122 | if let borderColor { 123 | fw.setCornerLayer(rectCorner, radius: cornerRadius, borderColor: borderColor, width: borderWidth) 124 | } else { 125 | fw.setCornerLayer(rectCorner, radius: cornerRadius) 126 | } 127 | layoutSubviewsBlock?(self) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Module/View/GradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientView.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2023/1/4. 6 | // 7 | 8 | import QuartzCore 9 | import UIKit 10 | 11 | // MARK: - GradientView 12 | /// 渐变View,无需设置渐变Layer的frame等,支持自动布局 13 | open class GradientView: UIView { 14 | /// 渐变Layer 15 | open var gradientLayer: CAGradientLayer { 16 | layer as! CAGradientLayer 17 | } 18 | 19 | /// 渐变色,CGColor数组 20 | open var colors: [Any]? { 21 | get { gradientLayer.colors } 22 | set { gradientLayer.colors = newValue } 23 | } 24 | 25 | /// 渐变位置 26 | open var locations: [NSNumber]? { 27 | get { gradientLayer.locations } 28 | set { gradientLayer.locations = newValue } 29 | } 30 | 31 | /// 渐变开始点 32 | open var startPoint: CGPoint { 33 | get { gradientLayer.startPoint } 34 | set { gradientLayer.startPoint = newValue } 35 | } 36 | 37 | /// 渐变结束点 38 | open var endPoint: CGPoint { 39 | get { gradientLayer.endPoint } 40 | set { gradientLayer.endPoint = newValue } 41 | } 42 | 43 | /// 初始化并指定渐变颜色、位置和渐变方向 44 | public convenience init(colors: [UIColor]?, locations: [NSNumber]?, startPoint: CGPoint, endPoint: CGPoint) { 45 | self.init(frame: .zero) 46 | setColors(colors, locations: locations, startPoint: startPoint, endPoint: endPoint) 47 | } 48 | 49 | /// 指定layerClass为CAGradientLayer 50 | override open class var layerClass: AnyClass { 51 | CAGradientLayer.self 52 | } 53 | 54 | /// 设置渐变颜色、位置和渐变方向 55 | open func setColors(_ colors: [UIColor]?, locations: [NSNumber]?, startPoint: CGPoint, endPoint: CGPoint) { 56 | gradientLayer.colors = colors?.compactMap { $0.cgColor } 57 | gradientLayer.locations = locations 58 | gradientLayer.startPoint = startPoint 59 | gradientLayer.endPoint = endPoint 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Module/ViewModel/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 可监听ViewModel协议,被View持有(Controller和View都视为View层),负责处理数据并通知View,兼容UIKit和SwiftUI使用 11 | public protocol ObservableViewModel: ObservableObject {} 12 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Plugin/Preview/ImagePreviewPluginImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePreviewPluginImpl.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/23. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 默认图片预览插件 11 | open class ImagePreviewPluginImpl: NSObject, ImagePreviewPlugin, @unchecked Sendable { 12 | // MARK: - Accessor 13 | /// 单例模式 14 | @objc(sharedInstance) 15 | public static let shared = ImagePreviewPluginImpl() 16 | 17 | /// 自定义图片预览控制器句柄,默认nil时使用自带控制器,显示分页,点击图片|视频时关闭,present样式为zoom 18 | open var previewControllerBlock: (@MainActor @Sendable () -> ImagePreviewController)? 19 | 20 | /// 图片预览全局自定义句柄,show方法自动调用 21 | open var customBlock: (@MainActor @Sendable (ImagePreviewController) -> Void)? 22 | 23 | // MARK: - ImagePreviewPlugin 24 | open func showImagePreview( 25 | imageURLs: [Any], 26 | imageInfos: [Any]?, 27 | currentIndex: Int, 28 | sourceView: (@MainActor @Sendable (Int) -> Any?)?, 29 | placeholderImage: (@MainActor @Sendable (Int) -> UIImage?)?, 30 | renderBlock: (@MainActor @Sendable (UIView, Int) -> Void)?, 31 | customBlock: (@MainActor @Sendable (Any) -> Void)? = nil, 32 | in viewController: UIViewController 33 | ) { 34 | var previewController: ImagePreviewController 35 | if let previewControllerBlock { 36 | previewController = previewControllerBlock() 37 | } else { 38 | previewController = ImagePreviewController() 39 | previewController.showsPageLabel = true 40 | previewController.dismissingWhenTappedImage = true 41 | previewController.dismissingWhenTappedVideo = true 42 | previewController.presentingStyle = .zoom 43 | } 44 | 45 | previewController.imagePreviewView.placeholderImage = placeholderImage 46 | previewController.imagePreviewView.renderZoomImageView = renderBlock 47 | previewController.sourceImageView = sourceView 48 | previewController.imagePreviewView.imageURLs = imageURLs 49 | previewController.imagePreviewView.imageInfos = imageInfos 50 | 51 | self.customBlock?(previewController) 52 | customBlock?(previewController) 53 | previewController.imagePreviewView.currentImageIndex = currentIndex 54 | viewController.present(previewController, animated: true) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/FWUIKit/Plugin/View/ViewPluginImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPluginImpl.swift 3 | // FWFramework 4 | // 5 | // Created by wuyong on 2022/8/22. 6 | // 7 | 8 | import UIKit 9 | #if FWMacroSPM 10 | @_spi(FW) import FWFramework 11 | #endif 12 | 13 | /// 默认视图插件 14 | open class ViewPluginImpl: NSObject, ViewPlugin, @unchecked Sendable { 15 | // MARK: - Accessor 16 | /// 单例模式 17 | @objc(sharedInstance) 18 | public static let shared = ViewPluginImpl() 19 | 20 | /// 自定义进度视图生产句柄,默认nil时ProgressView 21 | open var customProgressView: (@MainActor @Sendable (ProgressViewStyle) -> (UIView & ProgressViewPlugin)?)? 22 | 23 | /// 自定义指示器视图生产句柄,默认nil时UIActivityIndicatorView 24 | open var customIndicatorView: (@MainActor @Sendable (IndicatorViewStyle) -> (UIView & IndicatorViewPlugin)?)? 25 | 26 | // MARK: - ViewPlugin 27 | open func progressView(style: ProgressViewStyle) -> UIView & ProgressViewPlugin { 28 | let progressView = customProgressView?(style) ?? ProgressView() 29 | if let indicatorSize = style.indicatorSize { 30 | progressView.indicatorSize = indicatorSize 31 | } 32 | if let indicatorColor = style.indicatorColor { 33 | progressView.indicatorColor = indicatorColor 34 | } 35 | return progressView 36 | } 37 | 38 | open func indicatorView(style: IndicatorViewStyle) -> UIView & IndicatorViewPlugin { 39 | let indicatorView = customIndicatorView?(style) ?? UIActivityIndicatorView.fw.indicatorView(color: nil) 40 | if let indicatorSize = style.indicatorSize { 41 | indicatorView.indicatorSize = indicatorSize 42 | } 43 | if let indicatorColor = style.indicatorColor { 44 | indicatorView.indicatorColor = indicatorColor 45 | } 46 | return indicatorView 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPITypeReasons 9 | 10 | CA92.1 11 | 12 | NSPrivacyAccessedAPIType 13 | NSPrivacyAccessedAPICategoryUserDefaults 14 | 15 | 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | 35F9.1 19 | 20 | NSPrivacyAccessedAPIType 21 | NSPrivacyAccessedAPICategorySystemBootTime 22 | 23 | 24 | NSPrivacyAccessedAPITypeReasons 25 | 26 | E174.1 27 | 28 | NSPrivacyAccessedAPIType 29 | NSPrivacyAccessedAPICategoryDiskSpace 30 | 31 | 32 | NSPrivacyAccessedAPITypeReasons 33 | 34 | C617.1 35 | 36 | NSPrivacyAccessedAPIType 37 | NSPrivacyAccessedAPICategoryFileTimestamp 38 | 39 | 40 | 41 | 42 | --------------------------------------------------------------------------------