├── .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 | [](http://cocoadocs.org/docsets/FWFramework/)
4 | [](https://swiftpackageindex.com/lszzy/FWFramework)
5 | [](https://swiftpackageindex.com/lszzy/FWFramework)
6 | [](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 |
--------------------------------------------------------------------------------