├── .github ├── FUNDING.yml └── workflows │ └── CommitChecks.yml ├── .gitignore ├── .spi.yml ├── Development ├── .gitignore ├── Development │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Sources │ │ ├── BookBinding.swift │ │ ├── BookLongList.swift │ │ ├── BookReadingPropertyWrapper.swift │ │ ├── BookStoreReader.swift │ │ ├── ContentView.swift │ │ └── DevelopmentApp.swift │ ├── Tests │ │ └── DevelopmentTests.swift │ └── UITests │ │ ├── ReadingTests.swift │ │ └── StoreReaderTests.swift ├── DevelopmentUITests │ ├── DevelopmentUITests.swift │ └── DevelopmentUITestsLaunchTests.swift ├── Project.swift ├── Tuist.swift ├── Tuist │ ├── Package.resolved │ └── Package.swift └── mise.toml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Verge │ ├── Derived │ │ ├── Derived+Assign.swift │ │ └── Derived.swift │ ├── Documentation.docc │ │ ├── Activity.md │ │ ├── Changes.md │ │ ├── ComputedProperty.md │ │ ├── Derived.md │ │ ├── Dispatcher.md │ │ ├── Essentials │ │ │ └── Motivation.md │ │ ├── Guides │ │ │ ├── Advanced Usage.md │ │ │ ├── Basic Usage.md │ │ │ └── Migration Guide v9.md │ │ ├── Images │ │ │ └── changes@2x.png │ │ ├── Mutation.md │ │ ├── Resources │ │ │ └── Tiny.md │ │ ├── State.md │ │ ├── Verge.Store.md │ │ └── Verge.md │ ├── Library │ │ ├── BackgroundDeallocationQueue.swift │ │ ├── CachedMap.swift │ │ ├── EventEmitter.swift │ │ ├── InoutRef.swift │ │ ├── Log.swift │ │ ├── Signpost.swift │ │ ├── StoreActivitySubscription.swift │ │ ├── StoreStateSubscription.swift │ │ ├── StoreSubscriptionBase.swift │ │ ├── VergeAnyCancellable.swift │ │ ├── VergeConcurrency+SynchronizationTracker.swift │ │ ├── VergeConcurrency.swift │ │ └── _BackingStorage+.swift │ ├── Logging │ │ ├── ActivityTrace.swift │ │ ├── DefaultStoreLogger.swift │ │ ├── MutationTrace.swift │ │ ├── RuntimeError.swift │ │ ├── RuntimeSanitizer.swift │ │ └── StoreLogger.swift │ ├── Sendable.swift │ ├── Store │ │ ├── AnyTargetQueue.swift │ │ ├── Changes.swift │ │ ├── DetachedDispatcher.swift │ │ ├── KeyObject.swift │ │ ├── NonAtomicCounter.swift │ │ ├── Pipeline.swift │ │ ├── Scan.swift │ │ ├── StateType.swift │ │ ├── Store+Combine.swift │ │ ├── Store+RunLoop.swift │ │ ├── Store.swift │ │ ├── StoreDriverType+Accumulator.swift │ │ ├── StoreDriverType.swift │ │ ├── StoreMiddleware.swift │ │ ├── StoreOperation.swift │ │ ├── StoreType+Assignee.swift │ │ ├── StoreType+BindingDerived.swift │ │ ├── StoreType+Derived.swift │ │ ├── StoreWrapperType.swift │ │ ├── Transaction.swift │ │ └── UIStateStore.swift │ ├── SwiftUI │ │ ├── .swift │ │ ├── OnReceive.swift │ │ ├── Reading.swift │ │ ├── StoreObject.swift │ │ └── StoreReader.swift │ ├── Utility │ │ ├── Edge.swift │ │ ├── ReferenceEdge.swift │ │ └── ThunkToMainActor.swift │ ├── Verge.swift │ └── macros.swift ├── VergeClassic │ ├── Emitter.swift │ ├── Extensions.swift │ ├── Info.plist │ ├── Storage+Rx.swift │ ├── Storage.swift │ ├── Verge+Extension.swift │ ├── Verge.h │ ├── Verge@2x.png │ ├── VergeClassic.swift │ └── demo.gif ├── VergeMacros │ └── Source.swift ├── VergeMacrosPlugin │ ├── KeyPathMap.swift │ ├── MacroError.swift │ └── Plugin.swift ├── VergeNormalizationDerived │ ├── DerivedMaking+.swift │ ├── DerivedResult.swift │ ├── EntityType+Typealias.swift │ ├── EntityWrapper.swift │ ├── NonNullEntityWrapper.swift │ ├── Pipelines.swift │ ├── StoreType+.swift │ └── VergeNormalizationDerived.swift ├── VergeRx │ ├── Extensions.swift │ └── Store+Rx.swift └── VergeTiny │ └── Source.swift ├── StoreReaderDemo ├── StoreReaderDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── StoreReaderDemo │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── StoreReaderDemoApp.swift ├── TaskManagerPlayground ├── TaskManagerPlayground.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── TaskManagerPlayground │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Book.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── TaskManagerPlaygroundApp.swift ├── Tests ├── All.xctestplan ├── DemoState.swift ├── VergeMacrosTests │ └── KeyPathMapTests.swift ├── VergeNormalizationDerivedTests │ ├── CombiningTests.swift │ ├── DemoState.swift │ └── VergeNormalizationDerivedTests.swift ├── VergeRxTests │ ├── ChangedOperatorTests.swift │ ├── DemoState.swift │ ├── ReproduceDeadlockTests.swift │ ├── SubjectCompletionTests.swift │ └── VergeRxTests.swift ├── VergeTests │ ├── AccumulationTests.swift │ ├── ActivityTests.swift │ ├── BindingDerivedTests.swift │ ├── CachedMapTests.swift │ ├── ChangesTests.swift │ ├── ComparerTests.swift │ ├── ConcurrencyTests.swift │ ├── CopyPerformance.swift │ ├── CounterTests.swift │ ├── DemoState.swift │ ├── DerivedTests.swift │ ├── EdgeTests.swift │ ├── EventEmitterTests.swift │ ├── FilterTests.swift │ ├── IsolatedContextTests.swift │ ├── OldComparer.swift │ ├── PerformanceTests.swift │ ├── PipelineTests.swift │ ├── ReferenceEdgeTests.swift │ ├── Retain │ │ ├── PublisherCompletionTests.swift │ │ └── StoreSinkSusbscriptionTests.swift │ ├── RunLoopTests.swift │ ├── Sample.swift │ ├── StateTypeTests.swift │ ├── StoreAndDerivedTests.swift │ ├── StoreInitTests.swift │ ├── StoreMiddlewareTests.swift │ ├── StoreSinkTests.swift │ ├── StoreTaskTests.swift │ ├── SynchronizeDisplayValueTests.swift │ ├── SyntaxTests.swift │ ├── TransactionTests.swift │ ├── Usage.swift │ └── VergeStoreTests.swift └── VergeTinyTests │ └── VergeTinyTests.swift ├── Verge.playground ├── Pages │ ├── Memo.xcplaygroundpage │ │ └── Contents.swift │ ├── PlainStorePattern1.xcplaygroundpage │ │ └── Contents.swift │ ├── PlainStorePattern2.xcplaygroundpage │ │ └── Contents.swift │ ├── PlainStorePattern3.xcplaygroundpage │ │ └── Contents.swift │ ├── PlainStorePattern4.xcplaygroundpage │ │ └── Contents.swift │ ├── PlainStorePattern5.xcplaygroundpage │ │ └── Contents.swift │ ├── VergeStorePartern2.xcplaygroundpage │ │ └── Contents.swift │ └── VergeStorePattern.xcplaygroundpage │ │ └── Contents.swift ├── Sources │ └── Wire.swift └── contents.xcplayground ├── mise.toml └── playgrounds ├── .gitignore └── PlaySwiftUI ├── PlaySwiftUI.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── swiftpm │ └── Package.resolved └── PlaySwiftUI ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── ContentView.swift ├── PlaySwiftUIApp.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json └── Simple.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [muukii] 2 | patreon: muukii 3 | ko_fi: muukii 4 | -------------------------------------------------------------------------------- /.github/workflows/CommitChecks.yml: -------------------------------------------------------------------------------- 1 | name: CommitChecks 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | 8 | jobs: 9 | test: 10 | runs-on: macos-15 11 | 12 | steps: 13 | - uses: maxim-lobanov/setup-xcode@v1.1 14 | with: 15 | xcode-version: "16.2" 16 | - uses: actions/checkout@v2 17 | - name: Run Test 18 | run: set -o pipefail && xcodebuild -scheme Verge-Package test -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.1' -skipMacroValidation -skipPackagePluginValidation | xcbeautify 19 | 20 | ui-test: 21 | runs-on: macos-15 22 | defaults: 23 | run: 24 | working-directory: ./Development 25 | 26 | steps: 27 | - uses: maxim-lobanov/setup-xcode@v1.1 28 | with: 29 | xcode-version: "16.2" 30 | - uses: actions/checkout@v2 31 | - uses: jdx/mise-action@v2 32 | with: 33 | install: true 34 | cache: true 35 | experimental: true 36 | - run: tuist install 37 | - run: tuist generate --no-open 38 | - run: tuist test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | /worktree 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | 14 | .swiftpm 15 | 16 | ## Various settings 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | xcuserdata/ 26 | 27 | ## Other 28 | *.moved-aside 29 | *.xccheckout 30 | *.xcscmblueprint 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # 44 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | .build/ 48 | 49 | # CocoaPods - Refactored to standalone file 50 | 51 | Pods 52 | 53 | # Carthage - Refactored to standalone file 54 | 55 | Carthage 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | .DS_Store 70 | 71 | # Tuist 72 | Derived 73 | *.xcodeproj 74 | 75 | Workspace.xcworkspace/ 76 | 77 | # End of https://www.gitignore.io/api/swift 78 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Verge, VergeNormalizationDerived] 5 | -------------------------------------------------------------------------------- /Development/.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Xcode ### 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ### Xcode Patch ### 55 | *.xcodeproj/* 56 | !*.xcodeproj/project.pbxproj 57 | !*.xcodeproj/xcshareddata/ 58 | !*.xcworkspace/contents.xcworkspacedata 59 | /*.gcno 60 | 61 | ### Projects ### 62 | *.xcodeproj 63 | *.xcworkspace 64 | 65 | ### Tuist derived files ### 66 | graph.dot 67 | Derived/ 68 | 69 | ### Tuist managed dependencies ### 70 | Tuist/.build -------------------------------------------------------------------------------- /Development/Development/Resources/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 | -------------------------------------------------------------------------------- /Development/Development/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Development/Development/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Development/Development/Resources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Development/Development/Sources/BookBinding.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftUI 3 | import Verge 4 | 5 | struct BookBindingUsingReading: View { 6 | 7 | @ReadingObject> var state: BookBindingState 8 | 9 | init() { 10 | self._state = .init({ 11 | .init(initialState: .init()) 12 | }) 13 | } 14 | 15 | var body: some View { 16 | Counter(value: $state.value) 17 | } 18 | 19 | struct Counter: View { 20 | 21 | @Binding var value: Int 22 | 23 | var body: some View { 24 | VStack { 25 | Text("\(value)") 26 | Button { 27 | value += 1 28 | } label: { 29 | Text("Increment") 30 | } 31 | } 32 | } 33 | } 34 | 35 | } 36 | 37 | struct BookBindingUsingStoreReader: View { 38 | 39 | let store: Store = .init(initialState: .init()) 40 | 41 | init() { 42 | } 43 | 44 | var body: some View { 45 | StoreReader(store) { $state in 46 | Counter(value: $state.value) 47 | } 48 | } 49 | 50 | struct Counter: View { 51 | 52 | @Binding var value: Int 53 | 54 | var body: some View { 55 | VStack { 56 | Text("\(value)") 57 | Button { 58 | value += 1 59 | } label: { 60 | Text("Increment") 61 | } 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | @Tracking 69 | struct BookBindingState { 70 | 71 | var value: Int = 0 72 | 73 | } 74 | 75 | #Preview("Binding Reading") { 76 | BookBindingUsingReading() 77 | } 78 | 79 | #Preview("Binding StoreReader") { 80 | BookBindingUsingStoreReader() 81 | } 82 | -------------------------------------------------------------------------------- /Development/Development/Sources/BookLongList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookLongList.swift 3 | // Development 4 | // 5 | // Created by Muukii on 2025/03/26. 6 | // 7 | 8 | import SwiftUI 9 | import Verge 10 | 11 | @Tracking 12 | struct BookState { 13 | var items: [BookItem] = [] 14 | 15 | init(items: [BookItem]) { 16 | self.items = items 17 | } 18 | } 19 | 20 | struct BookItem: Identifiable { 21 | var id: some Hashable { 22 | cellStore 23 | } 24 | let cellStore: Store 25 | } 26 | 27 | @Tracking 28 | struct BookCellState { 29 | let id: Int 30 | var title: String 31 | var isSelected: Bool = false 32 | } 33 | 34 | struct BookCellContent: View { 35 | 36 | let store: Store 37 | 38 | var body: some View { 39 | StoreReader(store) { $state in 40 | RoundedRectangle(cornerRadius: 8) 41 | .fill(state.isSelected ? Color.red : Color.blue.opacity(0.2)) 42 | .aspectRatio(1, contentMode: .fit) 43 | .overlay( 44 | Text("\(state.id + 1)") 45 | .font(.system(size: 16)) 46 | ) 47 | } 48 | } 49 | } 50 | 51 | struct BookCell: View { 52 | 53 | let store: Store 54 | 55 | var body: some View { 56 | Button { 57 | store.commit { state in 58 | state.isSelected.toggle() 59 | } 60 | } label: { 61 | BookCellContent(store: store) 62 | } 63 | } 64 | } 65 | 66 | struct BookLongList: View { 67 | private let columns = [ 68 | GridItem(.flexible()), 69 | GridItem(.flexible()), 70 | GridItem(.flexible()), 71 | GridItem(.flexible()), 72 | ] 73 | 74 | @ReadingObject>({ 75 | .init(initialState: .init(items: (0..<500).map { index in 76 | BookItem( 77 | cellStore: Store( 78 | initialState: BookCellState( 79 | id: index, 80 | title: UUID().uuidString 81 | ) 82 | ) 83 | ) 84 | })) 85 | }) var state: BookState 86 | 87 | init() { 88 | 89 | } 90 | 91 | var body: some View { 92 | ScrollView { 93 | LazyVGrid(columns: columns, spacing: 16) { 94 | ForEach(state.items) { item in 95 | BookCell(store: item.cellStore) 96 | } 97 | } 98 | .padding() 99 | } 100 | } 101 | } 102 | 103 | #Preview { 104 | BookLongList() 105 | } 106 | -------------------------------------------------------------------------------- /Development/Development/Sources/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct ContentView: View { 4 | public init() {} 5 | 6 | public var body: some View { 7 | NavigationStack { 8 | 9 | List { 10 | 11 | NavigationLink { 12 | BookReading() 13 | } label: { 14 | Text("@Reading") 15 | } 16 | 17 | NavigationLink { 18 | PassedContainer() 19 | } label: { 20 | Text("@Reading - passed") 21 | } 22 | 23 | NavigationLink { 24 | StoreReaderSolution() 25 | } label: { 26 | Text("StoreReader") 27 | } 28 | 29 | NavigationLink { 30 | BookLongList() 31 | } label: { 32 | Text("Long List") 33 | } 34 | 35 | NavigationLink { 36 | BookBindingUsingReading() 37 | } label: { 38 | Text("Binding @Reading") 39 | } 40 | 41 | NavigationLink { 42 | BookBindingUsingStoreReader() 43 | } label: { 44 | Text("Binding StoreReader") 45 | } 46 | } 47 | 48 | } 49 | } 50 | } 51 | 52 | struct ContentView_Previews: PreviewProvider { 53 | static var previews: some View { 54 | ContentView() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Development/Development/Sources/DevelopmentApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct DevelopmentApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Development/Development/Tests/DevelopmentTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | final class DevelopmentTests: XCTestCase { 5 | func test_twoPlusTwo_isFour() { 6 | XCTAssertEqual(2+2, 4) 7 | } 8 | } -------------------------------------------------------------------------------- /Development/Development/UITests/ReadingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ReadingTests: XCTestCase { 4 | 5 | var app: XCUIApplication! 6 | 7 | override func setUpWithError() throws { 8 | continueAfterFailure = false 9 | app = XCUIApplication() 10 | app.launch() 11 | } 12 | 13 | override func tearDownWithError() throws { 14 | app = nil 15 | } 16 | 17 | func testA_up() { 18 | 19 | let app = XCUIApplication() 20 | app.collectionViews.buttons[ 21 | "@Reading" 22 | ] 23 | .tap() 24 | 25 | app.buttons["A Up"].tap() 26 | 27 | XCTAssertTrue(app.staticTexts["A Value: 1"].exists) 28 | XCTAssertTrue(app.staticTexts["B Value: 1"].exists) 29 | 30 | } 31 | 32 | func test_sync() { 33 | 34 | let app = XCUIApplication() 35 | app.collectionViews.buttons[ 36 | "@Reading" 37 | ] 38 | .tap() 39 | app.buttons["A.1"].tap() 40 | 41 | app.buttons["A.1.a: 0"].tap() 42 | 43 | XCTAssertTrue(app.buttons["A.1.a: 1"].exists) 44 | 45 | app.buttons["B.1"].tap() 46 | 47 | XCTAssertTrue(app.buttons["B.1.a: 1"].exists) 48 | 49 | app.buttons["B.1.a: 1"].tap() 50 | 51 | XCTAssertTrue(app.buttons["A.1.a: 2"].exists) 52 | XCTAssertTrue(app.buttons["B.1.a: 2"].exists) 53 | 54 | } 55 | 56 | func test_binding_reading() { 57 | 58 | let app = XCUIApplication() 59 | app.collectionViews/*@START_MENU_TOKEN@*/.buttons["Binding @Reading"]/*[[".cells.buttons[\"Binding @Reading\"]",".buttons[\"Binding @Reading\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() 60 | app.buttons["Increment"].tap() 61 | 62 | XCTAssertTrue(app.staticTexts["1"].exists) 63 | 64 | } 65 | 66 | func test_binding_storeReader() { 67 | 68 | let app = XCUIApplication() 69 | app.collectionViews.buttons["Binding StoreReader"].tap() 70 | app.buttons["Increment"].tap() 71 | 72 | XCTAssertTrue(app.staticTexts["1"].exists) 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Development/Development/UITests/StoreReaderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class StoreReaderTests: XCTestCase { 4 | 5 | var app: XCUIApplication! 6 | 7 | override func setUpWithError() throws { 8 | continueAfterFailure = false 9 | app = XCUIApplication() 10 | app.launch() 11 | } 12 | 13 | override func tearDownWithError() throws { 14 | app = nil 15 | } 16 | 17 | func testA_up() { 18 | 19 | let app = XCUIApplication() 20 | app.collectionViews.buttons[ 21 | "StoreReader" 22 | ] 23 | .tap() 24 | 25 | app.buttons["A Up"].tap() 26 | 27 | XCTAssertTrue(app.staticTexts["A Value: 1"].exists) 28 | XCTAssertTrue(app.staticTexts["B Value: 1"].exists) 29 | 30 | } 31 | 32 | func test_sync() { 33 | 34 | let app = XCUIApplication() 35 | app.collectionViews.buttons[ 36 | "StoreReader" 37 | ] 38 | .tap() 39 | app.buttons["A.1"].tap() 40 | 41 | app.buttons["A.1.a: 0"].tap() 42 | 43 | XCTAssertTrue(app.buttons["A.1.a: 1"].exists) 44 | 45 | app.buttons["B.1"].tap() 46 | 47 | XCTAssertTrue(app.buttons["B.1.a: 1"].exists) 48 | 49 | app.buttons["B.1.a: 1"].tap() 50 | 51 | XCTAssertTrue(app.buttons["A.1.a: 2"].exists) 52 | XCTAssertTrue(app.buttons["B.1.a: 2"].exists) 53 | 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Development/DevelopmentUITests/DevelopmentUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DevelopmentUITests.swift 3 | // DevelopmentUITests 4 | // 5 | // Created by Muukii on 2025/03/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class DevelopmentUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTApplicationLaunchMetric()]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Development/DevelopmentUITests/DevelopmentUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DevelopmentUITestsLaunchTests.swift 3 | // DevelopmentUITests 4 | // 5 | // Created by Muukii on 2025/03/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class DevelopmentUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Development/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let project = Project( 4 | name: "Development", 5 | targets: [ 6 | .target( 7 | name: "Development", 8 | destinations: .iOS, 9 | product: .app, 10 | bundleId: "io.tuist.Development", 11 | infoPlist: .extendingDefault( 12 | with: [ 13 | "UILaunchScreen": [ 14 | "UIColorName": "", 15 | "UIImageName": "", 16 | ] 17 | ] 18 | ), 19 | sources: ["Development/Sources/**"], 20 | resources: ["Development/Resources/**"], 21 | dependencies: [ 22 | .external(name: "Verge") 23 | ] 24 | ), 25 | .target( 26 | name: "DevelopmentUITests", 27 | destinations: .iOS, 28 | product: .uiTests, 29 | bundleId: "io.tuist.DevelopmentUITests", 30 | infoPlist: .default, 31 | sources: ["Development/UITests/**"], 32 | resources: [], 33 | dependencies: [.target(name: "Development")] 34 | ), 35 | .target( 36 | name: "DevelopmentTests", 37 | destinations: .iOS, 38 | product: .unitTests, 39 | bundleId: "io.tuist.DevelopmentTests", 40 | infoPlist: .default, 41 | sources: ["Development/Tests/**"], 42 | resources: [], 43 | dependencies: [.target(name: "Development")] 44 | ), 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /Development/Tuist.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let tuist = Tuist(project: .tuist()) -------------------------------------------------------------------------------- /Development/Tuist/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "fb710b9bf77d01a2d48fc3e3b210bfb0e949769f26d1ca1c351ac86995dca6fb", 3 | "pins" : [ 4 | { 5 | "identity" : "normalization", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/VergeGroup/Normalization", 8 | "state" : { 9 | "revision" : "6e7cb1ddeda4d0f1d2fbf8ca6d25ecd8ed6ba917", 10 | "version" : "1.1.0" 11 | } 12 | }, 13 | { 14 | "identity" : "rxswift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/ReactiveX/RxSwift.git", 17 | "state" : { 18 | "revision" : "5dd1907d64f0d36f158f61a466bab75067224893", 19 | "version" : "6.9.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-atomics", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-atomics.git", 26 | "state" : { 27 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 28 | "version" : "1.2.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-collections", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-collections", 35 | "state" : { 36 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 37 | "version" : "1.1.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-concurrency-task-manager", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/VergeGroup/swift-concurrency-task-manager", 44 | "state" : { 45 | "revision" : "340cf14e0282977deeeb436605d1810ce4f4fbc9", 46 | "version" : "1.4.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-macro-state-struct", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/VergeGroup/swift-macro-state-struct", 53 | "state" : { 54 | "revision" : "b6ade33024ae04699fa6a4885be70c0299eb3cec", 55 | "version" : "2.0.0" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-syntax", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-syntax.git", 62 | "state" : { 63 | "revision" : "0687f71944021d616d34d922343dcef086855920", 64 | "version" : "600.0.1" 65 | } 66 | }, 67 | { 68 | "identity" : "typedcomparator", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/VergeGroup/TypedComparator", 71 | "state" : { 72 | "revision" : "337ce0e573e7637ddd29392cb88c3b852f042c8e", 73 | "version" : "1.0.0" 74 | } 75 | }, 76 | { 77 | "identity" : "typedidentifier", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/VergeGroup/TypedIdentifier", 80 | "state" : { 81 | "revision" : "284340409ba47858a1b3c353dcef9c21303953db", 82 | "version" : "2.0.3" 83 | } 84 | } 85 | ], 86 | "version" : 3 87 | } 88 | -------------------------------------------------------------------------------- /Development/Tuist/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | import PackageDescription 3 | 4 | #if TUIST 5 | import struct ProjectDescription.PackageSettings 6 | 7 | let packageSettings = PackageSettings( 8 | // Customize the product types for specific package product 9 | // Default is .staticFramework 10 | // productTypes: ["Alamofire": .framework,] 11 | productTypes: [:] 12 | ) 13 | #endif 14 | 15 | let package = Package( 16 | name: "Development", 17 | dependencies: [ 18 | // Add your own dependencies here: 19 | // .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), 20 | // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies 21 | .package(path: "../../") 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Development/mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | tuist = "4.44.3" 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 muukii 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 | -------------------------------------------------------------------------------- /Sources/Verge/Derived/Derived+Assign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(Muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | extension StoreDriverType { 23 | 24 | /** 25 | Assigns a Store's state to a property of a store. 26 | 27 | - Returns: a cancellable. See detail of handling cancellable from ``StoreSubscription``'s docs 28 | */ 29 | public func assign( 30 | queue: some TargetQueueType = .passthrough, 31 | to binder: @escaping @Sendable (Changes) -> Void 32 | ) -> StoreStateSubscription { 33 | store.asStore().sinkState(queue: queue, receive: binder) 34 | } 35 | 36 | /** 37 | Assigns a Store's state to a property of a store. 38 | 39 | - Returns: a cancellable. See detail of handling cancellable from ``StoreSubscription``'s docs 40 | */ 41 | public func assign( 42 | queue: some MainActorTargetQueueType, 43 | to binder: @escaping @MainActor (Changes) -> Void 44 | ) -> StoreStateSubscription { 45 | store.asStore().sinkState(queue: queue, receive: binder) 46 | } 47 | 48 | } 49 | 50 | #if canImport(Combine) 51 | 52 | import Combine 53 | 54 | @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) 55 | extension Publisher { 56 | 57 | /** 58 | Assigns a Publishers's value to a property of a store. 59 | 60 | - Attention: Store won't be retained. 61 | - Returns: a cancellable. See detail of handling cancellable from `VergeAnyCancellable`'s docs 62 | - Author: Verge 63 | */ 64 | public func assign( 65 | to binder: @escaping (Output) -> Void 66 | ) -> VergeAnyCancellable { 67 | let cancellable = sink(receiveCompletion: { _ in }) { (value) in 68 | binder(value) 69 | } 70 | return .init { 71 | cancellable.cancel() 72 | } 73 | } 74 | 75 | } 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Activity.md: -------------------------------------------------------------------------------- 1 | # Activity 2 | 3 | ## What Activity brings to us 4 | 5 | Activity enables Event-driven partially. 6 | 7 | Verge supports to send any events that won’t be stored persistently. Even if an application runs with State-Driven, it might have some issues that not easy to something with State-Driven. 8 | 9 | For example, something that would happen with the timer’s trigger. It’s probably not easy to expressing that as a state. 10 | In this case, Activity helps that can do easily. 11 | 12 | This means Verge can use Event-Driven from Data-Driven partially. 13 | We think it’s not so special concept. SwiftUI supports these use cases as well that using Combine’s Publisher. 14 | 15 | ```swift 16 | func onReceive

(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> some View where P : Publisher, P.Failure == Never 17 | ``` 18 | 19 | [Apple’s SwiftUI Ref](https://developer.apple.com/documentation/swiftui/view/3365935-onreceive) 20 | 21 | ## Add Activity to the Store 22 | 23 | In sample code following this: 24 | 25 | ```swift 26 | final class MyStore: StoreComponentType { 27 | 28 | struct State { 29 | ... 30 | } 31 | 32 | } 33 | ``` 34 | 35 | To enable using Activity, we add new decralation just like this: 36 | 37 | ```swift 38 | final class MyStore: StoreComponentType { 39 | 40 | struct State { 41 | ... 42 | } 43 | 44 | /// 👇 45 | enum Activity { 46 | case didSendMessage 47 | } 48 | 49 | } 50 | ``` 51 | 52 | ## Send an Activity 53 | 54 | And finally, that Store now can emit an activity that we created. 55 | 56 | ```swift 57 | extension MyStore { 58 | func sendMessage() { 59 | send(.didSendMessage) 60 | } 61 | } 62 | ``` 63 | 64 | --- 65 | 66 | ## Subscribe the Activity 67 | 68 | **Normal** 69 | 70 | ```swift 71 | store.sinkActivity { activity in 72 | ... 73 | } 74 | .store(in: &subscriptions) 75 | ``` 76 | 77 | **Using Combine** 78 | 79 | ```swift 80 | store 81 | .activityPublisher 82 | .sink { event in 83 | // do something 84 | } 85 | .store(in: &subscriptions) 86 | ``` 87 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Changes.md: -------------------------------------------------------------------------------- 1 | # ``Verge/Changes`` 2 | 3 | `Changes` wraps primitive value inside and previous one. 4 | This data structure enables to get differences between now one and previous one. 5 | It helps us to prevent duplicated operation with the same value from the state. 6 | 7 | ![Changes structure](changes) 8 | 9 | ## How we update UI with the state uniquely in UIKit 10 | 11 | In subscribing the state and binding UI, it’s most important to reduce the meaningless time to update UI. 12 | What things are the meaningless? that is the update UI operations which contains no updates. 13 | Basically, we can prevent this with like followings: 14 | 15 | ```swift 16 | func updateUI(newState: State) { 17 | if self.label.text != newState.name { 18 | self.label.text = newState.name 19 | } 20 | } 21 | ``` 22 | 23 | Although, this approach make the code a little bit complicated by increasing the code that updates UI. 24 | Especially, same words come up a lot. 25 | 26 | ## Changes simplify the code 27 | 28 | Actually, state property of Store returns `Changes` implicitly. 29 | From the power of `dynamicMemberLookup`, we can use the property of the State with we don’t know that is actually Changes object. 30 | 31 | How to know there is differences is using `ifChanged`. 32 | 33 | ```swift 34 | let store: MyStore 35 | let state: Changes = store.state 36 | 37 | state.ifChanged(\.name) { name in 38 | // called when `name` changed only 39 | } 40 | ``` 41 | 42 | ## Patterns of `ifChanged` 43 | 44 | `ifChanged` has a bunch of overloads. 45 | Let’s take a look of patterns. 46 | 47 | ```swift 48 | state.ifChanged(\.aaa, \.bbb) { aaa, bbb in 49 | // Executes every two properties change. 50 | } 51 | ``` 52 | 53 | ```swift 54 | state.ifChanged({ "Mr." + $0.name }) { composedName in 55 | 56 | } 57 | ``` 58 | 59 | ```swift 60 | state.ifChanged({ "Mr." + $0.name }, { $0 == $1 }) { aaa, bbb in 61 | 62 | } 63 | ``` 64 | 65 | ```swift 66 | state.ifChanged({ ($0.aaa, $0.bbb, "Mr" + $0.name)}, ==) { composedName in 67 | // Returning tuple that contains composed value enables to do anything with the value you want. 68 | // It releases us from complicated streaming mixing. 69 | // Current Swift version does not support tuple conforming with Equatable, 70 | // You need passing `==` function in the second argument. 71 | } 72 | ``` 73 | 74 | ## Subscribing the state 75 | 76 | ```swift 77 | class ViewController: UIViewController { 78 | 79 | var subscriptions = Set() 80 | 81 | let store: MyStore 82 | 83 | override func viewDidLoad() { 84 | 85 | super.viewDidLoad() 86 | 87 | store.sinkChanges { [weak self] (changes) in 88 | // it will be called on the thread which committed 89 | self?.update(changes: changes) 90 | } 91 | .store(in: &subscriptions) 92 | } 93 | 94 | private func update(changes: Changes { 95 | changes.ifChanged(\.name) { name in 96 | // called only name changed 97 | } 98 | ... 99 | } 100 | 101 | } 102 | ``` 103 | 104 | ## Make Changes object the first-time value 105 | 106 | If you have a `Changes` from anywhere, it might have previous value, 107 | Using `ifChanged` might return false because compared with the previous one. 108 | You can create the Changed object that always returns true from `ifChanged` with followings: 109 | 110 | ```swift 111 | let changes: Changes 112 | 113 | let firstTimeChanges: Changes = changes.droppedPrevious() 114 | ```# <#Title#> 115 | 116 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Essentials/Motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | ## Verge focuses use-cases in the real-world 4 | 5 | Recently, we could say the unidirectional data flow is a popular architecture such as flux. 6 | 7 | ## Does flux architecture have a good performance? 8 | 9 | It depends. The performance will be the worst depends on how it is used. 10 | 11 | However, most of the cases, we don't know the app we're creating how it will grow and scales.While the application is scaling up, the performance might decrease by getting complexity.To keep performance, we need to tune it up with several approaches.Considering the performance takes time from the beginning.it will make us be annoying to use flux architecture. 12 | 13 | ## Verge is designed for use from small and supports to scale. 14 | 15 | Setting Verge up quickly, and tune-up when we need it. 16 | 17 | Verge automatically tune-up and shows us what makes performance badly while development from Xcode's documentation. 18 | 19 | For example, Verge provides these stuff to tune performance up. 20 | 21 | - Derived (Similar to [facebookexperimental/Recoil](https://github.com/facebookexperimental/Recoil)'s Selector) 22 | - ORM 23 | 24 | ## Supports volatile events - Activity 25 | 26 | We use an event as `Activity` that won't be stored in the state.This concept would help us to describe something that is not easy to describe as a state in the client application. 27 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Guides/Advanced Usage.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | ## To keep performance and scalability 4 | 5 | ## Adding a cachable computed property in a State 6 | 7 | We can add a computed property in a state to get a derived value with stored property, 8 | and that computed property works fine as well other stored property. 9 | 10 | ```swift 11 | struct MyState { 12 | var items: [Item] = [] { 13 | 14 | var itemsCount: Int { 15 | items.count 16 | } 17 | } 18 | ``` 19 | 20 | However, this patterns might cause an expensive cost of operation depends on how they computes. 21 | To solve it, Verge arrows us to define the computed property with another approach. 22 | 23 | ```swift 24 | struct MyState: ExtendedStateType { 25 | 26 | var name: String = ... 27 | var items: [Int] = [] 28 | 29 | struct Extended: ExtendedType { 30 | let filteredArray = Field.Computed<[Int]> { 31 | $0.items.filter { $0 > 300 } 32 | } 33 | .ifChanged(selector: \.largeArray) 34 | } 35 | } 36 | ``` 37 | 38 | ```swift 39 | let store: MyStore 40 | 41 | store.changes.computed.filteredArray 42 | ``` 43 | 44 | This defined computed array calculates only if changed specified value. 45 | That condition to re-calculate is defined with `.ifChanged` method in the example code. 46 | 47 | And finally, it caches the result by first-time access and it returns cached value until if the source value changed. 48 | 49 | ## Making a slice of the state (Selector) 50 | 51 | We can create a slice object that derives a data from the state. 52 | 53 | ```swift 54 | let derived: Derived = store.derived(.map(\.count)) 55 | 56 | // take a value 57 | derived.value 58 | 59 | // subscribe a value changes 60 | derived.sinkChanges { (changes: Changes) in 61 | } 62 | ``` 63 | 64 | ## Creating a Dispatcher 65 | 66 | Store arrows us to define an action in itself, that might cause gain complexity in supporting a large application. 67 | To solve this, Verge offers us to create an object that dispatches an action to the store. 68 | We can separate the code of actions to keep maintainability. 69 | that also help us to manage a different type of dependencies. 70 | 71 | For example, the case of those dependencies different between logged-in and logged-out. 72 | 73 | ```swift 74 | class MyDispatcher: MyStore.Dispatcher { 75 | func moreOperation() { 76 | commit { 77 | ... 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | ```swift 84 | let store: MyStore 85 | let dispatcher = MyDispatcher(target: store) 86 | 87 | dispatcher.moreOperation() 88 | ``` 89 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Guides/Migration Guide v9.md: -------------------------------------------------------------------------------- 1 | # Changes in v9 2 | 3 | ## Store requires Equatable State 4 | 5 | From v8 complex implementations, v9 becomes it requires Equatable to State associated with Store, Changes, Derived and what else related. 6 | 7 | Then Verge v9 now dropped lots of implementations and overloads covering cases if the state don’t have Equatable. 8 | 9 | ## Store can have multiple databases 10 | 11 | Now, Store has `databases` accessor that allows us to read database. 12 | `databases` is `DatabaseDynamicMembers` which provides property following to State shape. 13 | This looks up member only type of `DatabaseType` 14 | 15 | ```swift 16 | @dynamicMemberLookup 17 | public struct DatabaseDynamicMembers { 18 | 19 | unowned let store: Store 20 | 21 | init(store: Store) { 22 | self.store = store 23 | } 24 | 25 | public subscript(dynamicMember keyPath: KeyPath) -> DatabaseContext { 26 | .init(keyPath: keyPath, store: store) 27 | } 28 | 29 | } 30 | ``` 31 | 32 | ## Use new syntax for creating Field.Computed 33 | 34 | As you know, Changes supports memoized-computed-property. 35 | That can be done writing `Field.Computed` 36 | 37 | Now its writing syntax will change. 38 | 39 | ```swift 40 | let filteredArray = Field.Computed( 41 | .map( 42 | using: { $0.largeArray }, 43 | transform: { $0.filter { $0 > 300 } } 44 | ) 45 | ) 46 | ``` 47 | 48 | `using` specifies the dependencies which used from `transform` function. 49 | `transform` function will create a new value from given dependencies provided from `using` function. 50 | 51 | ## Detail changes 52 | 53 | - Dropped complex implementations related to performance tunings 54 | - Stopped using cache to return Derived internally. 55 | - Deleted `batchCommit` 56 | 57 | From v8 complex implementations, v9 becomes it requires Equatable to State associated with Store, Changes, Derived and what else related. 58 | 59 | Then Verge v9 now dropped lots of implementations and overloads covering cases if the state don’t have Equatable. 60 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Images/changes@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VergeGroup/swift-verge/61e8909d36a430a6fcaba96dd8dc7a9bf260771f/Sources/Verge/Documentation.docc/Images/changes@2x.png -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Mutation.md: -------------------------------------------------------------------------------- 1 | # Mutation 2 | 3 | ## What Mutation is 4 | 5 | The only way to actually change state in a Store is by committing a mutation. Define a function that returns Mutation object. That expresses that function is Mutation 6 | 7 |

11 | 12 | Mutation does **NOT** allow to run asynchronous operation. 13 | 14 | ### Define mutations in the Store 15 | 16 | ```swift 17 | struct MyState { 18 | var todos: [TODO] = [] 19 | } 20 | 21 | class MyStore: Store { 22 | 23 | func addNewTodo(title: String) { 24 | commit { (state: inout InoutRef) in 25 | state.todos.append(Todo(title: title, hasCompleted: false)) 26 | } 27 | } 28 | 29 | } 30 | ``` 31 | 32 | 36 | 37 | ### Run Mutation 38 | 39 | ```swift 40 | let store = MyStore() 41 | store.addNewTodo(title: "Create SwiftUI App") 42 | 43 | print(store.state.todos) 44 | // store.state.todos => [Todo(title: "Create SwiftUI App", hasCompleted: false)] 45 | ``` 46 | 47 | ## Batches multiple commtis 48 | 49 | Committing multiple mutations in a short time might decrease performance. 50 | Because the subscribers around the store derive a state many times. 51 | 52 | Like this, 53 | 54 | ```swift 55 | class MyStore: Store { 56 | 57 | func myMutation() { 58 | if ... { 59 | commit { 60 | ... 61 | } 62 | // emits updated event 63 | } 64 | 65 | if ... { 66 | commit { 67 | ... 68 | } 69 | // emits updated event 70 | } 71 | 72 | if ... { 73 | commit { 74 | ... 75 | } 76 | // emits updated event 77 | } 78 | } 79 | 80 | } 81 | ``` 82 | 83 | To keep better performance, we need to keep using fewer commits in a short time. 84 | 85 | We have 2 ways. 86 | 87 | ### Using `commit` 88 | 89 | ``DispatcherType/commit(_:_:_:_:mutation:)`` provides ``InoutRef``, that can detect how the wrapped state will change. 90 | If there is no change, `commit` does nothing and no emitting the events from the Store. 91 | 92 | However, you should attention `commit` is atomically operation which means, the Store getting lock while committing. 93 | 94 | ```swift 95 | func myMutation() { 96 | commit { (state: inout InoutRef) in 97 | if ... { 98 | state.aaa = ... 99 | } 100 | 101 | if ... { 102 | state.bbb = ... 103 | } 104 | 105 | if ... { 106 | state.ccc = ... 107 | } 108 | } 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Resources/Tiny.md: -------------------------------------------------------------------------------- 1 | # Yet another super tiny store pattern with Verge/Tiny 2 | 3 | In fact, `store-pattern` doesn't need something library to run. 4 | The actually necessary thing is **the changing detection in UIKit.** 5 | 6 | Without the changing detection, the code is here. 7 | There is no dependencies. 8 | 9 | ```swift 10 | class MyView: UIView { 11 | private struct State { 12 | var count: Int = 0 13 | } 14 | 15 | private var state: State { 16 | didSet { 17 | update(with: state) 18 | } 19 | } 20 | 21 | private func update(with state: State) { 22 | ... 23 | } 24 | } 25 | ``` 26 | 27 | Next, we focus on `update(with:)` method. 28 | Try to simulate updating the label's value. 29 | 30 | ```swift 31 | private func update(with state: State) { 32 | myLabel.text = "\(state.count)" 33 | } 34 | ``` 35 | 36 | As you can see, you will think you want to prevent updating the value until the value changed. 37 | 38 | ## Use Verge.Tiny module to prevent the duplicated updating. 39 | 40 | With installing `Verge/Tiny` module, we can write up like followings. 41 | 42 | ```swift 43 | private func update(with state: State) { 44 | associatedProperties.doIfChanged(state.count) { count in 45 | myLabel.text = "\(count)" 46 | } 47 | } 48 | ``` 49 | 50 | `associatedProperties` is a storage of the values that associated with its owner object(NSObject). 51 | 52 | `doIfChanged` gets the location of the code that would be a unique key by composition in the storage. 53 | 54 | With this functions, we can get a filter anywhere in the object. 55 | However, this function might affect code readabilities in Swift. 56 | Please carefully using this. 57 | 58 | We recommend you gather those operations into one place. 59 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/State.md: -------------------------------------------------------------------------------- 1 | # Thinking in single state tree (Not enforced) 2 | 3 | VergeStore uses a **single state-tree. (Recommended)** That means an object contains all of the application’s state. With this, we can get to achieve **“single source of truth”** 4 | 5 | That state is managed by ``Store``. It process updating the state and notify updated events to the subscribers. 6 | 7 | > Tip: Store DOES support multiple state-tree as well. Depending on the case, we can create another Store instance. 8 | 9 | ## Add a computed property 10 | 11 | ``` 12 | struct State: Equatable { 13 | 14 | var count: Int = 0 15 | 16 | var countText: String { 17 | return count.description 18 | } 19 | 20 | } 21 | ``` 22 | 23 | Extending properties that computes a value from stored property. 24 | 25 | Although in some of cases, the cost of computing might be higher which depends on how it create the value from stored properties. 26 | 27 | There is ``ExtendedStateType``. 28 | This provies us to get more stuff that **increases performance** and productivity. 29 | 30 | ## Attention to Normalization 31 | 32 | **If you put the data that has relation-ship or complicated structure into state tree, it would be needed normalization to keep performance. Please check VergeORM module** 33 | 34 | [About more Normalization and why we need to do this](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape/) 35 | -------------------------------------------------------------------------------- /Sources/Verge/Documentation.docc/Verge.Store.md: -------------------------------------------------------------------------------- 1 | # ``Verge/Store`` 2 | 3 | - Store: 4 | - Should be a reference type object, 5 | - To share the state they manage to multiple subscribers. 6 | - Receives **Mutation** to update the state with thread-safety 7 | - Compatible with SwiftUI’s observableObject and we can use `StateReader` to read the state partially. 8 | 9 | ## Ways to creating a store 10 | 11 | Verge provides 2 ways to create a store. 12 | 13 | 1. Declare a class that conforms with `StoreComponentType` - It’s a protocol that indicates the class wraps a store inside and behaves like a store. 14 | 2. Subclassing from `Store` - a most basic way, but we need to define State and Activity outside 15 | 16 | Now, we recommend using No.1 in order to manage the source code with better portability. 17 | 18 | ## Declare a class that conforms with `StoreComponentType` 19 | 20 | ```swift 21 | final class MyStore: StoreComponentType { 22 | 23 | struct State: StateType { 24 | var count: Int = 0 25 | } 26 | 27 | /// This means wrapping store inside. (Probably it should be renamed as like `innerStore` or `wrappedStore`) 28 | /// `DefaultStore` is a typealias that declared by `StoreComponentType`. 29 | /// You can use any class that inherited from `Store` for your use-cases. 30 | let store: DefaultStore 31 | 32 | init() { 33 | 34 | self.store = .init(initialState: .init()) 35 | 36 | } 37 | 38 | } 39 | ``` 40 | 41 | ### Add a Mutation 42 | 43 | ```swift 44 | extension MyStore { 45 | 46 | func increment() { 47 | commit { 48 | $0.count += 0 49 | } 50 | } 51 | 52 | } 53 | ``` 54 | 55 | ### Commit the mutation 56 | 57 | ```swift 58 | let store = MyStore() 59 | store.increment() 60 | ``` 61 | 62 | ## Subclassing from `Store` 63 | 64 | ```swift 65 | struct State: StateType { 66 | var count: Int = 0 67 | } 68 | 69 | enum Activity { 70 | case happen 71 | } 72 | 73 | final class MyStore: Store { 74 | 75 | init() { 76 | super.init( 77 | initialState: .init(), 78 | logger: DefaultStoreLogger.shared 79 | ) 80 | } 81 | 82 | } 83 | ``` 84 | 85 | ### Add a Mutation 86 | 87 | ```swift 88 | extension MyStore { 89 | 90 | func increment() { 91 | commit { 92 | $0.count += 0 93 | } 94 | } 95 | 96 | } 97 | ``` 98 | 99 | ### Commit the mutation 100 | 101 | ```swift 102 | let store = MyStore() 103 | store.increment() 104 | ``` 105 | -------------------------------------------------------------------------------- /Sources/Verge/Library/BackgroundDeallocationQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(Muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import DequeModule 24 | 25 | actor BackgroundDeallocationQueue { 26 | 27 | private var buffer: Deque> = .init() 28 | 29 | func releaseObjectInBackground(object: AnyObject) { 30 | 31 | let innerCurrentRef = Unmanaged.passRetained(object) 32 | 33 | let isFirstEntry = buffer.isEmpty 34 | buffer.append(innerCurrentRef) 35 | 36 | if isFirstEntry { 37 | Task { 38 | // accumulate objects to dealloc for batching 39 | try? await Task.sleep(nanoseconds: 1_000_000) 40 | await self.drain() 41 | } 42 | } 43 | } 44 | 45 | func drain() async { 46 | 47 | guard buffer.isEmpty == false else { 48 | return 49 | } 50 | 51 | while let pointer = buffer.popFirst() { 52 | pointer.release() 53 | await Task.yield() 54 | } 55 | 56 | await drain() 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Verge/Library/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // VergeCore 4 | // 5 | // Created by muukii on 2020/02/24. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import os.log 12 | 13 | enum Log { 14 | 15 | static let store = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "store") }) 16 | 17 | static let storeCommit = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "store.commit") }) 18 | 19 | static let storeReader = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "storeReader") }) 20 | 21 | static let reading = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "reading") }) 22 | 23 | static let writeGraph = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "writeGraph") }) 24 | 25 | } 26 | 27 | extension OSLog { 28 | 29 | @inline(__always) 30 | fileprivate static func makeOSLogInDebug(isEnabled: Bool = true, _ factory: () -> OSLog) -> OSLog { 31 | #if DEBUG 32 | return factory() 33 | #else 34 | return .disabled 35 | #endif 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Verge/Library/Signpost.swift: -------------------------------------------------------------------------------- 1 | import os 2 | import Foundation 3 | 4 | nonisolated(unsafe) 5 | public var _verge_signpost_enabled = ProcessInfo.processInfo.environment["VERGE_SIGNPOST_ENABLED"] != nil 6 | 7 | @usableFromInline 8 | enum SignpostConstants { 9 | @usableFromInline 10 | static let performanceLog = { () -> OSLog in 11 | if #available(iOSApplicationExtension 13.0, *) { 12 | return OSLog(subsystem: "lib.verge", category: "performance") 13 | } else { 14 | return OSLog(subsystem: "lib.verge", category: "performance") 15 | } 16 | }() 17 | @usableFromInline 18 | static let pointOfInterestLog = OSLog(subsystem: "lib.verge", category: .pointsOfInterest) 19 | } 20 | 21 | @inlinable 22 | public func vergeSignpostEvent(_ event: StaticString) { 23 | #if DEBUG 24 | if _verge_signpost_enabled { 25 | let id = OSSignpostID(log: SignpostConstants.pointOfInterestLog) 26 | os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: event, signpostID: id) 27 | } 28 | #endif 29 | } 30 | 31 | @inlinable 32 | public func vergeSignpostEvent(_ event: StaticString, label: @autoclosure () -> String) { 33 | #if DEBUG 34 | if _verge_signpost_enabled { 35 | let id = OSSignpostID(log: SignpostConstants.pointOfInterestLog) 36 | os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: event, signpostID: id, "%@", label()) 37 | } 38 | #endif 39 | } 40 | 41 | 42 | public struct VergeSignpostTransaction { 43 | 44 | #if DEBUG 45 | @usableFromInline 46 | let _end: () -> Void 47 | public let rawID: os_signpost_id_t 48 | #endif 49 | 50 | public init(_ name: StaticString) { 51 | #if DEBUG 52 | if _verge_signpost_enabled { 53 | let id = OSSignpostID(log: SignpostConstants.performanceLog) 54 | self.rawID = id.rawValue 55 | os_signpost(.begin, log: SignpostConstants.performanceLog, name: name, signpostID: id) 56 | _end = { 57 | os_signpost(.end, log: SignpostConstants.performanceLog, name: name, signpostID: id) 58 | } 59 | } else { 60 | rawID = 0 61 | _end = {} 62 | } 63 | #else 64 | #endif 65 | } 66 | 67 | public init(_ name: StaticString, label: @autoclosure () -> String) { 68 | #if DEBUG 69 | if _verge_signpost_enabled { 70 | let id = OSSignpostID(log: SignpostConstants.performanceLog) 71 | self.rawID = id.rawValue 72 | let _label = label() 73 | os_signpost(.begin, log: SignpostConstants.performanceLog, name: name, signpostID: id, "Begin: %@", _label) 74 | _end = { 75 | os_signpost(.end, log: SignpostConstants.performanceLog, name: name, signpostID: id, "End: %@", _label) 76 | } 77 | } else { 78 | rawID = 0 79 | _end = {} 80 | } 81 | #else 82 | #endif 83 | } 84 | 85 | public func event(name: StaticString, label: @autoclosure () -> String) { 86 | #if DEBUG 87 | if _verge_signpost_enabled { 88 | let id = OSSignpostID(rawID) 89 | os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: name, signpostID: id, "%@", label()) 90 | } 91 | #endif 92 | } 93 | 94 | @inlinable 95 | @inline(__always) 96 | public func event(name: StaticString) { 97 | #if DEBUG 98 | if _verge_signpost_enabled { 99 | let id = OSSignpostID(rawID) 100 | os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: name, signpostID: id) 101 | } 102 | #endif 103 | } 104 | 105 | @inlinable 106 | @inline(__always) 107 | public func end() { 108 | #if DEBUG 109 | _end() 110 | #endif 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Verge/Library/StoreActivitySubscription.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Atomics 3 | 4 | /** 5 | A subscription that is compatible with Combine’s Cancellable. 6 | You can manage asynchronous tasks either call the ``cancel()`` to halt the subscription, or allow it to terminate upon instance deallocation, and by implementing the ``storeWhileSourceActive()`` technique, the subscription’s active status is maintained until the source store is released. 7 | */ 8 | public final class StoreActivitySubscription: StoreSubscriptionBase, @unchecked Sendable { 9 | 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Sources/Verge/Library/StoreStateSubscription.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Atomics 3 | 4 | /** 5 | A subscription that is compatible with Combine's Cancellable. 6 | You can manage asynchronous tasks either call the ``cancel()`` to halt the subscription, or allow it to terminate upon instance deallocation, and by implementing the ``storeWhileSourceActive()`` technique, the subscription's active status is maintained until the source store is released. 7 | */ 8 | public final class StoreStateSubscription: StoreSubscriptionBase, @unchecked Sendable { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Verge/Library/StoreSubscriptionBase.swift: -------------------------------------------------------------------------------- 1 | 2 | public class StoreSubscriptionBase: Hashable, Cancellable { 3 | 4 | public static func == (lhs: StoreSubscriptionBase, rhs: StoreSubscriptionBase) -> Bool { 5 | lhs === rhs 6 | } 7 | 8 | private struct State { 9 | var wasCancelled: Bool = false 10 | weak var storeCancellable: VergeAnyCancellable? 11 | var associatedStore: (any StoreType)? 12 | } 13 | 14 | private let state: VergeConcurrency.ManagedCriticalState 15 | 16 | public func hash(into hasher: inout Hasher) { 17 | ObjectIdentifier(self).hash(into: &hasher) 18 | } 19 | 20 | private let source: EventEmitterCancellable 21 | 22 | init( 23 | _ eventEmitterCancellable: EventEmitterCancellable, 24 | storeCancellable: VergeAnyCancellable 25 | ) { 26 | self.source = eventEmitterCancellable 27 | self.state = .init(State(storeCancellable: storeCancellable)) 28 | } 29 | 30 | public func cancel() { 31 | 32 | let continues = state.withCriticalRegion { state -> Bool in 33 | guard state.wasCancelled == false else { 34 | return false 35 | } 36 | state.wasCancelled = true 37 | // if it's associated as storeWhileSourceActive. 38 | state.storeCancellable?.dissociate(self) 39 | state.associatedStore = nil 40 | return true 41 | } 42 | 43 | guard continues else { 44 | return 45 | } 46 | 47 | source.cancel() 48 | } 49 | 50 | /** 51 | Make this subscription alive while the source is active. 52 | the source means a root data store which is Store. 53 | 54 | In case of Derived, the source will be Derived's upstream. 55 | If the upstream invalidated, this subscription will stop. 56 | */ 57 | @discardableResult 58 | public func storeWhileSourceActive() -> Self { 59 | state.withCriticalRegion { state in 60 | assert(state.wasCancelled == false) 61 | assert(state.storeCancellable != nil) 62 | state.storeCancellable?.associate(self) 63 | } 64 | return self 65 | } 66 | 67 | func associate(store: any StoreType) -> Self { 68 | state.withCriticalRegion { state in 69 | assert(state.wasCancelled == false) 70 | assert(state.storeCancellable != nil) 71 | state.associatedStore = store 72 | } 73 | return self 74 | } 75 | 76 | /** 77 | Converts to Combine.AnyCancellable to make it auto cancellable. 78 | */ 79 | public func asAny() -> AnyCancellable { 80 | return .init { [self] in 81 | self.cancel() 82 | } 83 | } 84 | 85 | deinit { 86 | cancel() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Verge/Library/VergeConcurrency+SynchronizationTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | extension VergeConcurrency { 25 | /// 26 | /// 27 | /// Modified based on RxSwift's original implementations. 28 | public final class SynchronizationTracker: @unchecked Sendable { 29 | 30 | public enum Warning: Hashable { 31 | case reentrancyAnomaly 32 | case synchronizationAnomaly 33 | } 34 | 35 | private let _lock = VergeConcurrency.RecursiveLock() 36 | 37 | private var _threads = [UnsafeMutableRawPointer: Int]() 38 | 39 | private let _isEnabled: Bool 40 | 41 | public init(debugOnly: Bool = false) { 42 | if debugOnly { 43 | #if DEBUG 44 | self._isEnabled = true 45 | #else 46 | self._isEnabled = false 47 | #endif 48 | } else { 49 | self._isEnabled = true 50 | } 51 | } 52 | 53 | /** 54 | Marks as entering a synchronized operation. 55 | */ 56 | @discardableResult 57 | public func register( 58 | _ file: StaticString = #file, 59 | _ function: StaticString = #function, 60 | _ line: UInt = #line, 61 | printsConsole: Bool = false 62 | ) -> Set { 63 | 64 | guard _isEnabled else { return .init() } 65 | 66 | self._lock.lock(); defer { self._lock.unlock() } 67 | 68 | var flags = Set() 69 | 70 | let pointer = Unmanaged.passUnretained(Thread.current).toOpaque() 71 | let count = (self._threads[pointer] ?? 0) + 1 72 | 73 | if count > 1 { 74 | flags.insert(.reentrancyAnomaly) 75 | } 76 | 77 | self._threads[pointer] = count 78 | 79 | if self._threads.count > 1 { 80 | flags.insert(.synchronizationAnomaly) 81 | } 82 | 83 | if printsConsole, flags.isEmpty == false { 84 | print("⚠️[SynchronizationTracker] Found issues \(flags) in \(file):\(function):\(line)") 85 | } 86 | 87 | return flags 88 | } 89 | 90 | /** 91 | Marks as exited a synchronized operation. 92 | */ 93 | public func unregister() { 94 | 95 | guard _isEnabled else { return } 96 | 97 | self._lock.lock(); defer { self._lock.unlock() } 98 | let pointer = Unmanaged.passUnretained(Thread.current).toOpaque() 99 | self._threads[pointer] = (self._threads[pointer] ?? 1) - 1 100 | if self._threads[pointer] == 0 { 101 | self._threads[pointer] = nil 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Verge/Library/_BackingStorage+.swift: -------------------------------------------------------------------------------- 1 | 2 | extension _BackingStorage { 3 | 4 | func map(_ transform: (borrowing Value) throws -> U) rethrows -> _BackingStorage { 5 | return .init( 6 | try transform(value) 7 | ) 8 | } 9 | 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Sources/Verge/Logging/ActivityTrace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | /// A trace that indicates the activity where comes from. 25 | public struct ActivityTrace: Encodable, Sendable { 26 | 27 | public let createdAt: Date = .init() 28 | public let name: String 29 | public let file: String 30 | public let function: String 31 | public let line: UInt 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Verge/Logging/MutationTrace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public protocol HasTraces { 25 | var traces: [MutationTrace] { get } 26 | } 27 | 28 | /// A trace that indicates the mutation where comes from. 29 | public struct MutationTrace: Encodable, Equatable, Sendable { 30 | 31 | public static func == (lhs: MutationTrace, rhs: MutationTrace) -> Bool { 32 | lhs.createdAt == rhs.createdAt && 33 | lhs.name == rhs.name && 34 | lhs.file.description == rhs.file.description && 35 | lhs.function.description == rhs.function.description && 36 | lhs.line == rhs.line 37 | } 38 | 39 | public let createdAt: Date = .init() 40 | public let name: String 41 | public let file: StaticString 42 | public let function: StaticString 43 | public let line: UInt 44 | 45 | public init( 46 | name: String = "", 47 | file: StaticString = #file, 48 | function: StaticString = #function, 49 | line: UInt = #line 50 | ) { 51 | self.name = name 52 | self.file = file 53 | self.function = function 54 | self.line = line 55 | } 56 | 57 | } 58 | 59 | extension StaticString: @retroactive Encodable { 60 | 61 | public func encode(to encoder: Encoder) throws { 62 | var container = encoder.singleValueContainer() 63 | try container.encode(description) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Verge/Logging/RuntimeError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | public enum RuntimeError: Swift.Error { 23 | 24 | case recoveredStateFromReceivingOlderVersion(latestState: AnyChangesType, receivedState: AnyChangesType) 25 | case recursiveleyCommit(storeName: String, traces: [MutationTrace]) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Verge/Logging/RuntimeSanitizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | public struct RuntimeSanitizer: Sendable { 23 | 24 | nonisolated(unsafe) 25 | public static var global = RuntimeSanitizer() 26 | 27 | public var isSanitizerStateReceivingByCorrectOrder: Bool = false 28 | public var isRecursivelyCommitDetectionEnabled: Bool = false 29 | 30 | public var onDidFindRuntimeError: @Sendable (RuntimeError) -> Void = { _ in } 31 | 32 | public init() { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Verge/Logging/StoreLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | /// A protocol to register logger and get the event VergeStore emits. 25 | public protocol StoreLogger { 26 | 27 | func didCommit(log: CommitLog, sender: AnyObject) 28 | func didSendActivity(log: ActivityLog, sender: AnyObject) 29 | 30 | func didCreateDispatcher(log: DidCreateDispatcherLog, sender: AnyObject) 31 | func didDestroyDispatcher(log: DidDestroyDispatcherLog, sender: AnyObject) 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/Verge/Sendable.swift: -------------------------------------------------------------------------------- 1 | 2 | final class UnsafeSendableClass: @unchecked Sendable { 3 | var value: T 4 | 5 | init(_ value: T) { 6 | self.value = value 7 | } 8 | } 9 | 10 | struct UnsafeSendableWeak: @unchecked Sendable { 11 | weak var value: T? 12 | 13 | init(_ value: T) { 14 | self.value = value 15 | } 16 | } 17 | 18 | struct UnsafeSendableStruct: ~Copyable, @unchecked Sendable { 19 | var value: T 20 | 21 | init(_ value: consuming T) { 22 | self.value = value 23 | } 24 | 25 | consuming func send() -> sending T { 26 | return value 27 | } 28 | 29 | consuming func with(_ mutation: (inout sending T) throws -> sending Return) rethrows -> sending Return { 30 | try mutation(&value) 31 | } 32 | } 33 | 34 | func withUnsafeSending(_ value: consuming T) -> sending T { 35 | UnsafeSendableStruct(value).send() 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Verge/Store/DetachedDispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | public final class DetachedDispatcher: StoreDriverType 23 | { 24 | 25 | public let store: Store 26 | public let scope: WritableKeyPath & Sendable 27 | 28 | init( 29 | store: Store, 30 | scope: WritableKeyPath & Sendable 31 | ) { 32 | self.store = store 33 | self.scope = scope 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Verge/Store/KeyObject.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | @_spi(Internal) 5 | public final class KeyObject: NSObject, NSCopying { 6 | 7 | public func copy(with zone: NSZone? = nil) -> Any { 8 | return KeyObject(content: content) 9 | } 10 | 11 | public let content: Content 12 | 13 | public init(content: consuming Content) { 14 | self.content = content 15 | } 16 | 17 | public override var hash: Int { 18 | content.hashValue 19 | } 20 | 21 | public override func isEqual(_ object: Any?) -> Bool { 22 | 23 | guard let other = object as? KeyObject else { 24 | return false 25 | } 26 | 27 | guard content == other.content else { return false } 28 | 29 | return true 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Verge/Store/NonAtomicCounter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(Muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | /// A container that manages raw value to describe mark as updated. 25 | public struct NonAtomicCounter: Hashable, Sendable { 26 | 27 | private(set) public var value: UInt64 = 0 28 | 29 | public init() {} 30 | 31 | public mutating func increment() { 32 | value &+= 1 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Verge/Store/Scan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(Muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public final class Scan: Sendable { 25 | 26 | public typealias Accumulator = @Sendable (inout Accumulate, Element) -> Void 27 | 28 | public var value: Accumulate { 29 | _value.value 30 | } 31 | 32 | private let _value: VergeConcurrency.UnfairLockAtomic 33 | private let _accumulator: Accumulator 34 | 35 | public init(seed: Accumulate, accumulator: @escaping Accumulator) { 36 | self._value = .init(seed) 37 | self._accumulator = accumulator 38 | } 39 | 40 | public func accumulate(_ element: Element) -> Accumulate { 41 | _value.modify { _v -> Accumulate in 42 | _accumulator(&_v, element) 43 | return _v 44 | } 45 | } 46 | 47 | } 48 | 49 | extension Scan { 50 | 51 | /// A Scan instance that increments itself integer value each event received 52 | public static func counter() -> Scan { 53 | .init(seed: 0, accumulator: { n, _ in n += 1 }) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Verge/Store/StateType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import StateStruct 24 | 25 | /** 26 | An opt-in protocol that indicates it's used as a state of the State. 27 | */ 28 | public protocol StateType { 29 | 30 | /** 31 | A chance of modifying state alongside the commit. 32 | You may use this to make another value that consists of the state's self. 33 | It's better to use it for better performance to get the value rather than using computed property. 34 | */ 35 | @Sendable 36 | static func reduce( 37 | modifying: inout Self, 38 | transaction: inout Transaction, 39 | current: Changes 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Verge/Store/Store+Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Combine 23 | 24 | @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) 25 | extension Store { 26 | 27 | /// A publisher that repeatedly emits the changes when state updated 28 | /// 29 | /// Guarantees to emit the first event on started subscribing. 30 | /// 31 | /// - Parameter startsFromInitial: Make the first changes object's hasChanges always return true. 32 | /// - Returns: 33 | @_spi(Package) 34 | public func _statePublisher() -> some Combine.Publisher, Never> { 35 | 36 | return 37 | publisher 38 | .associate(resource: self, retains: keepsAliveForSubscribers) 39 | .flatMap { event in 40 | guard case .state(.didUpdate(let state)) = event else { 41 | return Empty, Never>().eraseToAnyPublisher() 42 | } 43 | return Just>(state) 44 | .eraseToAnyPublisher() 45 | } 46 | .merge(with: Just(state.droppedPrevious())) 47 | 48 | } 49 | 50 | // @_spi(Package) 51 | public func _activityPublisher() -> some Combine.Publisher { 52 | 53 | return 54 | publisher 55 | .associate(resource: self, retains: keepsAliveForSubscribers) 56 | .flatMap { event in 57 | guard case .activity(let a) = event else { 58 | return Empty().eraseToAnyPublisher() 59 | } 60 | return Just(a).eraseToAnyPublisher() 61 | } 62 | } 63 | 64 | } 65 | 66 | extension Publisher { 67 | 68 | func associate(resource: AnyObject, retains: Bool) -> some Publisher { 69 | 70 | let box = ResourceBox(object: resource, retains: retains) 71 | 72 | return handleEvents(receiveCancel: { 73 | // retain self until subscription finsihed 74 | withExtendedLifetime(box) {} 75 | }) 76 | } 77 | 78 | } 79 | 80 | private final class ResourceBox { 81 | 82 | private let object: AnyObject? 83 | 84 | init(object: AnyObject, retains: Bool) { 85 | if retains { 86 | self.object = object 87 | } else { 88 | self.object = nil 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Verge/Store/Store+RunLoop.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Store { 4 | 5 | /// Push an event to run event loop. 6 | public func updateMainLoop() { 7 | RunLoop.main.perform(inModes: [.common]) {} 8 | } 9 | 10 | /** 11 | Subscribes state updates in given run-loop. 12 | */ 13 | @MainActor 14 | public func pollMainLoop(receive: @escaping @MainActor (Changes) -> Void) -> VergeAnyCancellable { 15 | 16 | var latestState: Changes? = nil 17 | 18 | let subscription = RunLoopActivityObserver.addObserver(acitivity: .beforeWaiting, in: .main) { 19 | 20 | MainActor.assumeIsolated { 21 | let newState = self.state 22 | 23 | guard (latestState?.version ?? 0) < newState.version else { 24 | return 25 | } 26 | 27 | let state: Changes 28 | 29 | if let latestState { 30 | state = newState.replacePrevious(latestState) 31 | } else { 32 | state = newState.droppedPrevious() 33 | } 34 | 35 | latestState = newState 36 | 37 | receive(state) 38 | } 39 | 40 | } 41 | 42 | let firstState = state.droppedPrevious() 43 | 44 | latestState = firstState 45 | 46 | receive(firstState) 47 | 48 | return .init { 49 | RunLoopActivityObserver.remove(subscription) 50 | } 51 | } 52 | 53 | } 54 | 55 | enum RunLoopActivityObserver { 56 | 57 | struct Subscription { 58 | let mode: CFRunLoopMode 59 | let observer: CFRunLoopObserver? 60 | weak var targetRunLoop: RunLoop? 61 | } 62 | 63 | static func addObserver(acitivity: CFRunLoopActivity, in runLoop: RunLoop, callback: @escaping () -> Void) -> Subscription { 64 | 65 | let o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, acitivity.rawValue, true, Int.max, { observer, activity in 66 | callback() 67 | }); 68 | 69 | assert(o != nil) 70 | 71 | let mode = CFRunLoopMode.commonModes! 72 | let cfRunLoop = runLoop.getCFRunLoop() 73 | 74 | CFRunLoopAddObserver(cfRunLoop, o, mode); 75 | 76 | return .init(mode: mode, observer: o, targetRunLoop: runLoop) 77 | } 78 | 79 | static func remove(_ subscription: consuming Subscription) { 80 | 81 | guard let observer = subscription.observer, let targetRunLoop = subscription.targetRunLoop else { 82 | return 83 | } 84 | 85 | CFRunLoopRemoveObserver(targetRunLoop.getCFRunLoop(), observer, subscription.mode); 86 | } 87 | 88 | } 89 | 90 | #if DEBUG && canImport(SwiftUI) 91 | import SwiftUI 92 | 93 | #Preview { 94 | Content() 95 | } 96 | 97 | @Tracking 98 | private struct StoreState { 99 | var count: Int = 0 100 | } 101 | 102 | private struct Content: View { 103 | 104 | let store = Store<_, Never>(initialState: StoreState()) 105 | 106 | @State var subscription: VergeAnyCancellable? 107 | @State var timer: Timer? 108 | 109 | var body: some View { 110 | VStack { 111 | Button("Up") { 112 | store.commit { 113 | $0.count += 1 114 | } 115 | } 116 | Button("Background Up") { 117 | 118 | for _ in 0..<10 { 119 | store.commit { 120 | $0.count += 1 121 | } 122 | } 123 | 124 | } 125 | Button("Run") { 126 | RunLoop.main.run(until: Date(timeIntervalSinceNow: 19)) 127 | } 128 | } 129 | .onAppear { 130 | subscription = store.pollMainLoop { state in 131 | print(state.count) 132 | } 133 | } 134 | } 135 | 136 | } 137 | #endif 138 | 139 | -------------------------------------------------------------------------------- /Sources/Verge/Store/StoreMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public protocol StoreMiddlewareType: Sendable { 25 | 26 | associatedtype State 27 | 28 | @Sendable 29 | func modify(modifyingState: inout State, transaction: inout Transaction, current: Changes) 30 | 31 | } 32 | 33 | public struct AnyStoreMiddleware: StoreMiddlewareType, Sendable { 34 | 35 | private let closure: @Sendable (_ modifyingState: inout State, _ transaction: inout Transaction, _ current: Changes) -> Void 36 | 37 | init( 38 | modify: @escaping @Sendable ( 39 | _ modifyingState: inout State, _ transaction: inout Transaction, _ current: Changes 40 | ) 41 | -> Void 42 | ) { 43 | self.closure = modify 44 | } 45 | 46 | public func modify( 47 | modifyingState: inout State, transaction: inout Transaction, current: Changes 48 | ) { 49 | self.closure(&modifyingState, &transaction, current) 50 | } 51 | 52 | } 53 | 54 | extension StoreMiddlewareType { 55 | 56 | /** 57 | Creates an instance that commits mutations according to the original committing. 58 | */ 59 | public static func modify( 60 | modify: @escaping @Sendable ( 61 | _ modifyingState: inout State, _ transaction: inout Transaction, _ current: Changes 62 | ) 63 | -> Void 64 | ) -> Self where Self == AnyStoreMiddleware { 65 | return .init(modify: modify) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Verge/Store/StoreOperation.swift: -------------------------------------------------------------------------------- 1 | import class Foundation.NSRecursiveLock 2 | 3 | public enum StoreOperation: Sendable { 4 | case nonAtomic 5 | case atomic(NSRecursiveLock) 6 | 7 | func lock() { 8 | switch self { 9 | case .nonAtomic: 10 | break 11 | case .atomic(let lock): 12 | lock.lock() 13 | } 14 | } 15 | 16 | func unlock() { 17 | switch self { 18 | case .nonAtomic: 19 | break 20 | case .atomic(let lock): 21 | lock.unlock() 22 | } 23 | } 24 | 25 | public static var atomic: Self { 26 | return .atomic(.init()) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Sources/Verge/Store/StoreWrapperType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(Muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | @available(*, deprecated, renamed: "StoreDriverType") 25 | public typealias StoreComponentType = StoreDriverType 26 | 27 | /// It would be deprecated in the future. 28 | @available(*, deprecated, renamed: "StoreDriverType") 29 | public typealias StoreWrapperType = StoreComponentType 30 | -------------------------------------------------------------------------------- /Sources/Verge/Store/Transaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | /** 25 | Value storage for commit operation. 26 | The commit operation accepts adding context about its operation. 27 | It brings contextual operation into state-driven. 28 | For instance, same value changes but it's not the same meaning actually. 29 | 30 | ``Changes`` has ``Changes/transaction`` property. 31 | */ 32 | public struct Transaction: Sendable { 33 | 34 | private var values: [ObjectIdentifier : Any & Sendable] = [:] 35 | 36 | /// - Attention: non-atomic property 37 | public private(set) var traces: [MutationTrace] = [] 38 | 39 | mutating func append(trace: MutationTrace) { 40 | traces.append(trace) 41 | } 42 | 43 | mutating func append(traces otherTraces: [MutationTrace]) { 44 | traces.append(contentsOf: otherTraces) 45 | } 46 | 47 | public subscript(key: K.Type) -> K.Value where K : TransactionKey { 48 | get { 49 | values[ObjectIdentifier(K.self)] as? K.Value ?? K.defaultValue 50 | } 51 | mutating set { 52 | values[ObjectIdentifier(K.self)] = newValue 53 | } 54 | } 55 | 56 | public init() { 57 | } 58 | 59 | } 60 | 61 | /** 62 | A type based key for transaction. 63 | It's like SwiftUI's EnvironmentValue. 64 | Making a new type as key, gat and set values over the key. 65 | It's much safer than using string directly as avoiding conflict by using same value. 66 | 67 | ``` 68 | enum MyKey: TransactionKey { 69 | static var defaultValue: String? { nil } 70 | } 71 | ``` 72 | 73 | ``` 74 | extension Transaction { 75 | var myValue: String? { 76 | get { 77 | self[MyKey.self] 78 | } 79 | set { 80 | self[MyKey.self] = newValue 81 | } 82 | } 83 | } 84 | ``` 85 | */ 86 | public protocol TransactionKey { 87 | 88 | associatedtype Value: Sendable 89 | 90 | static var defaultValue: Value { get } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Verge/Store/UIStateStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 muukii 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | /// A store that optimized for only using in UI thread. 23 | /// No using locks. 24 | @MainActor 25 | public final class UIStateStore: Store, @unchecked Sendable { 26 | 27 | public nonisolated init( 28 | initialState: State, 29 | logger: StoreLogger? = nil 30 | ) { 31 | super.init( 32 | initialState: initialState, 33 | storeOperation: .nonAtomic, 34 | logger: logger 35 | ) 36 | } 37 | 38 | } 39 | 40 | @propertyWrapper 41 | @MainActor 42 | public struct UIState: Sendable { 43 | 44 | private let store: UIStateStore 45 | 46 | public nonisolated init(wrappedValue: State) { 47 | self.store = .init(initialState: wrappedValue) 48 | } 49 | 50 | // MARK: - PropertyWrapper 51 | 52 | public var wrappedValue: State { 53 | get { store.state.primitive } 54 | nonmutating set { 55 | store.commit { 56 | $0 = newValue 57 | } 58 | } 59 | } 60 | 61 | public var projectedValue: UIStateStore { 62 | return store 63 | } 64 | 65 | } 66 | 67 | @propertyWrapper 68 | public struct AtomicState: Sendable { 69 | 70 | private let store: Store 71 | 72 | public nonisolated init(wrappedValue: State) { 73 | self.store = .init(initialState: wrappedValue) 74 | } 75 | 76 | // MARK: - PropertyWrapper 77 | 78 | public var wrappedValue: State { 79 | get { store.state.primitive } 80 | nonmutating set { 81 | store.commit { 82 | $0 = newValue 83 | } 84 | } 85 | } 86 | 87 | public var projectedValue: Store { 88 | return store 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Verge/SwiftUI/.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Untitled.swift 3 | // Verge 4 | // 5 | // Created by Muukii on 2025/03/19. 6 | // 7 | 8 | -------------------------------------------------------------------------------- /Sources/Verge/SwiftUI/OnReceive.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | 5 | /// Adds an action to perform when the specified `DispatcherType` publishes a state change. 6 | /// 7 | /// Use `onReceiveState(_ instance: perform:)` to perform an action when a `DispatcherType` instance publishes a state change. 8 | /// The system calls the `perform` closure on the main thread each time the `State` changes. 9 | /// 10 | /// struct MyView: View { 11 | /// 12 | /// let viewModel: MyViewModel 13 | /// 14 | /// var body: some View { 15 | /// Text("Hello, World!") 16 | /// .onReceiveState(viewModel) { state in 17 | /// // Handle state changes here 18 | /// } 19 | /// } 20 | /// } 21 | /// 22 | /// - Parameters: 23 | /// - instance: The `DispatcherType` instance to subscribe to. 24 | /// - perform: A closure to execute when the `State` changes. 25 | public func onReceiveState( 26 | _ instance: D, 27 | perform: @escaping @MainActor (Changes) -> Void 28 | ) -> some View { 29 | onReceive( 30 | instance.store.asStore()._statePublisher().receive(on: DispatchQueue.main), 31 | perform: { value in 32 | MainActor.assumeIsolated { 33 | perform(value) 34 | } 35 | } 36 | ) 37 | } 38 | 39 | /// Adds an action to perform when the specified `DispatcherType` publishes an activity. 40 | /// 41 | /// Use `onReceiveActivity(_ instance: perform:)` to perform an action when a `DispatcherType` instance publishes an activity. 42 | /// The system calls the `perform` closure on the main thread each time the `Activity` is published. 43 | /// 44 | /// struct MyView: View { 45 | /// 46 | /// let viewModel: MyViewModel 47 | /// 48 | /// var body: some View { 49 | /// Text("Hello, World!") 50 | /// .onReceiveActivity(viewModel) { activity in 51 | /// // Handle activities here 52 | /// } 53 | /// } 54 | /// } 55 | /// 56 | /// - Parameters: 57 | /// - instance: The `DispatcherType` instance to subscribe to. 58 | /// - perform: A closure to execute when the `Activity` is published. 59 | public func onReceiveActivity( 60 | _ instance: D, 61 | perform: @escaping @MainActor (D.TargetStore.Activity) -> Void 62 | ) -> some View { 63 | onReceive( 64 | instance.store.asStore()._activityPublisher().receive(on: DispatchQueue.main), 65 | perform: { value in 66 | MainActor.assumeIsolated { 67 | perform(value) 68 | } 69 | } 70 | ) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Verge/SwiftUI/StoreObject.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /** 4 | A property wrapper that instantiates a `Store` for the view lifecycle. 5 | 6 | This property wrapper is designed to manage a `Store` object for the view lifecycle without making the view reactive to `Store` changes, which is the behavior of `@StateObject` with `ObservableObject`. This is because `Store` updates can be handled through `StoreReader`, and it's often undesirable to have the entire view refreshed whenever the `Store` updates. 7 | 8 | - Note: The `Store` is a type that conforms to `DispatcherType`. 9 | 10 | - Warning: This property wrapper should only be used for store objects that are expected to have a lifetime matching the lifetime of the view. 11 | */ 12 | @available(iOS 14, watchOS 7.0, tvOS 14, *) 13 | @MainActor 14 | @propertyWrapper 15 | public struct StoreObject: DynamicProperty { 16 | 17 | @StateObject private var backing: Wrapper 18 | 19 | /// The current value of the store object. 20 | public var wrappedValue: Store { 21 | self.backing.object 22 | } 23 | 24 | /// Creates a new store object. 25 | /// 26 | /// - Parameter thunk: A closure that creates the initial store. 27 | public init(wrappedValue thunk: @autoclosure @escaping () -> Store) { 28 | self._backing = .init(wrappedValue: .init(object: thunk())) 29 | } 30 | 31 | /// A wrapper for the `Store` that serves as a bridge to `ObservableObject`. 32 | private final class Wrapper: ObservableObject { 33 | let object: Store 34 | 35 | init(object: Store) { 36 | self.object = object 37 | } 38 | } 39 | } 40 | 41 | #if DEBUG 42 | 43 | @available(iOS 14, watchOS 7.0, tvOS 14, *) 44 | enum Preview_StoreObject: PreviewProvider { 45 | 46 | static var previews: some View { 47 | 48 | Group { 49 | Container() 50 | } 51 | 52 | } 53 | 54 | struct Container: View { 55 | 56 | @State var count = 0 57 | 58 | var body: some View { 59 | 60 | VStack { 61 | Button("Reset") { 62 | count += 1 63 | } 64 | Child() 65 | .id(count) 66 | } 67 | 68 | } 69 | 70 | } 71 | 72 | struct Child: View { 73 | 74 | @StoreObject var store: ViewModel = .init() 75 | 76 | var body: some View { 77 | let _ = print("render") 78 | VStack { 79 | Text("here is child") 80 | StoreReader(store) { $state in 81 | Text("count: \(state.count)") 82 | Text(state.count.description) 83 | } 84 | Button("up") { 85 | store.increment() 86 | } 87 | Button("up dummy") { 88 | store.incrementDummy() 89 | } 90 | } 91 | 92 | } 93 | } 94 | 95 | final class ViewModel: StoreDriverType { 96 | 97 | @Tracking 98 | struct State: Equatable { 99 | var count: Int = 0 100 | var count_dummy: Int = 0 101 | } 102 | 103 | let store: Store 104 | 105 | init() { 106 | self.store = .init(initialState: .init()) 107 | print("Init") 108 | } 109 | 110 | func increment() { 111 | commit { 112 | $0.count += 1 113 | } 114 | } 115 | 116 | func incrementDummy() { 117 | commit { 118 | $0.count_dummy += 1 119 | } 120 | } 121 | 122 | deinit { 123 | print("deinit") 124 | } 125 | } 126 | 127 | } 128 | 129 | #endif 130 | -------------------------------------------------------------------------------- /Sources/Verge/SwiftUI/StoreReader.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import StateStruct 4 | import SwiftUI 5 | 6 | /** 7 | A view that reads the state from Store and displays content according to the state. 8 | The view subscribes to the state updates and updates the content when the state changes. 9 | 10 | The state requires `@Tracking` macro to be used. 11 | 12 | ```swift 13 | @Tracking 14 | struct State { 15 | var count: Int = 0 16 | } 17 | ``` 18 | 19 | If you have nested types, you can use `@Tracking` macro to the nested types. 20 | Then the StoreReader can track through the nested types. 21 | 22 | ```swift 23 | @Tracking 24 | struct Nested { 25 | var count: Int = 0 26 | } 27 | 28 | @Tracking 29 | struct State { 30 | var nested: Nested = .init() 31 | } 32 | 33 | ## How to make Binding 34 | 35 | Use ``StoreBindable`` to make binding. 36 | 37 | ```swift 38 | @StoreBindable var store = store 39 | $store.count 40 | ``` 41 | */ 42 | @available(iOS 14, watchOS 7.0, tvOS 14, *) 43 | public struct StoreReader: View where Driver.TargetStore.State : TrackingObject { 44 | 45 | let storeReading: Reading 46 | 47 | private let file: StaticString 48 | private let line: UInt 49 | 50 | /// Needs to use Reading directly to provide the latest state when it's accessed. from escaping closure. 51 | private let content: (BindableReading>) -> Content 52 | 53 | /// Initialize from `Store` 54 | /// 55 | /// - Parameters: 56 | /// - store: 57 | /// - content: 58 | public init( 59 | file: StaticString = #file, 60 | line: UInt = #line, 61 | label: StaticString? = nil, 62 | _ driver: Driver, 63 | @ViewBuilder content: @escaping (BindableReading>) -> Content 64 | ) { 65 | self.file = file 66 | self.line = line 67 | self.storeReading = .init( 68 | file: file, 69 | line: line, 70 | label: label, 71 | driver 72 | ) 73 | self.content = content 74 | 75 | } 76 | 77 | public var body: some View { 78 | content(storeReading.projectedValue) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Verge/Utility/ThunkToMainActor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @preconcurrency 4 | @MainActor 5 | @inline(__always) 6 | func thunkToMainActor(_ run: @MainActor () throws -> Void) rethrows { 7 | assert(Thread.isMainThread) 8 | try run() 9 | } 10 | 11 | @preconcurrency 12 | @MainActor 13 | @inline(__always) 14 | func thunkToMainActor(_ run: @MainActor () -> Void) { 15 | assert(Thread.isMainThread) 16 | run() 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Verge/Verge.swift: -------------------------------------------------------------------------------- 1 | 2 | @_exported import ConcurrencyTaskManager 3 | @_exported import Combine 4 | @_exported import TypedComparator 5 | @_exported import VergeMacros 6 | @_exported import StateStruct 7 | 8 | -------------------------------------------------------------------------------- /Sources/Verge/macros.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Emitter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Activity.swift 3 | // Verge 4 | // 5 | // Created by muukii on 2019/07/16. 6 | // Copyright © 2019 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import RxRelay 12 | 13 | public final class Emitter { 14 | 15 | private var source: Signal { 16 | return emitter.asSignal() 17 | } 18 | 19 | private let emitter: PublishRelay = .init() 20 | 21 | public init() { 22 | 23 | } 24 | 25 | public func emit(onNext: ((Event) -> Void)? = nil, onCompleted: (() -> Void)? = nil, onDisposed: (() -> Void)? = nil) -> Disposable { 26 | return source.emit(onNext: onNext, onCompleted: onCompleted, onDisposed: onDisposed) 27 | } 28 | 29 | public func asSignal() -> Signal { 30 | return source 31 | } 32 | 33 | public func asObservable() -> Observable { 34 | return source.asObservable() 35 | } 36 | 37 | func makeEmitter() -> Accepter { 38 | return .init(backingEmitter: emitter) 39 | } 40 | } 41 | 42 | public struct Accepter { 43 | 44 | private let backingEmitter: PublishRelay 45 | 46 | fileprivate init(backingEmitter: PublishRelay) { 47 | self.backingEmitter = backingEmitter 48 | } 49 | 50 | public func accept(_ event: Event) { 51 | backingEmitter.accept(event) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Extensions.swift: -------------------------------------------------------------------------------- 1 | import RxSwift 2 | import Foundation 3 | import Verge 4 | 5 | nonisolated(unsafe) fileprivate var storage_subject: Void? 6 | 7 | extension ReadonlyStorage { 8 | 9 | private var subject: BehaviorSubject { 10 | 11 | objc_sync_enter(self) 12 | defer { 13 | objc_sync_exit(self) 14 | } 15 | 16 | if let associated = objc_getAssociatedObject(self, &storage_subject) as? BehaviorSubject { 17 | 18 | return associated 19 | 20 | } else { 21 | 22 | let associated = BehaviorSubject.init(value: value) 23 | objc_setAssociatedObject(self, &storage_subject, associated, .OBJC_ASSOCIATION_RETAIN) 24 | 25 | let lock = VergeConcurrency.RecursiveLock() 26 | 27 | sinkEvent { (event) in 28 | switch event { 29 | case .willUpdate: 30 | break 31 | case .didUpdate(let newValue): 32 | lock.lock(); defer { lock.unlock() } 33 | associated.onNext(newValue) 34 | case .willDeinit: 35 | lock.lock(); defer { lock.unlock() } 36 | associated.onCompleted() 37 | } 38 | } 39 | 40 | return associated 41 | } 42 | } 43 | 44 | /// Returns an observable sequence 45 | /// 46 | /// - Returns: Returns an observable sequence 47 | public func asObservable() -> Observable { 48 | subject.asObservable() 49 | } 50 | 51 | /// Returns an infallible sequence 52 | public func asInfallible() -> Infallible { 53 | subject.asInfallible(onErrorRecover: { _ in fatalError() }) 54 | } 55 | 56 | public func asObservable(keyPath: KeyPath) -> Observable { 57 | asObservable() 58 | .map { $0[keyPath: keyPath] } 59 | } 60 | 61 | /// Returns an infallible sequence 62 | public func asInfallible(keyPath: KeyPath) -> Infallible { 63 | subject.asInfallible(onErrorRecover: { _ in fatalError() }) 64 | .map { $0[keyPath: keyPath] } 65 | } 66 | 67 | public func asDriver() -> Driver { 68 | subject.asDriver(onErrorDriveWith: .empty()) 69 | } 70 | 71 | public func asDriver(keyPath: KeyPath) -> Driver { 72 | return asDriver() 73 | .map { $0[keyPath: keyPath] } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.0.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Storage+Rx.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | import RxSwift 5 | import RxCocoa 6 | 7 | #if !COCOAPODS 8 | import Verge 9 | import VergeRx 10 | #endif 11 | 12 | extension Storage: ReactiveCompatible {} 13 | 14 | nonisolated(unsafe) private var storage_subject: Void? 15 | nonisolated(unsafe) private var storage_diposeBag: Void? 16 | 17 | extension ReadonlyStorage { 18 | 19 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 20 | /// 21 | /// - Parameters: 22 | /// - selector: 23 | /// - comparer: 24 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 25 | public func changed(_ selector: @escaping (Value) -> S, _ comparer: @escaping (S, S) throws -> Bool) -> Observable { 26 | asObservable() 27 | .map { selector($0) } 28 | .distinctUntilChanged(comparer) 29 | } 30 | 31 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 32 | /// 33 | /// - Parameters: 34 | /// - selector: 35 | /// - comparer: 36 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 37 | public func changed(_ selector: @escaping (Value) -> S) -> Observable { 38 | changed(selector, ==) 39 | } 40 | 41 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 42 | /// 43 | /// - Parameters: 44 | /// - selector: 45 | /// - comparer: 46 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 47 | public func changedDriver(_ selector: @escaping (Value) -> S, _ comparer: @escaping (S, S) throws -> Bool) -> Driver { 48 | asObservable() 49 | .map { selector($0) } 50 | .distinctUntilChanged(comparer) 51 | .asDriver(onErrorRecover: { _ in .empty() }) 52 | } 53 | 54 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 55 | /// 56 | /// - Parameters: 57 | /// - selector: 58 | /// - comparer: 59 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 60 | public func changedDriver(_ selector: @escaping (Value) -> S) -> Driver { 61 | changedDriver(selector, ==) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Verge+Extension.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | import RxCocoa 5 | 6 | extension VergeType { 7 | 8 | public func commitBinder( 9 | name: String = "", 10 | description: String = "", 11 | file: StaticString = #file, 12 | function: StaticString = #function, 13 | line: UInt = #line, 14 | mutate: @escaping (inout State, S) -> Void 15 | ) -> Binder { 16 | 17 | return Binder(self) { t, e in 18 | t.commit(name, description, file, function, line) { s in 19 | mutate(&s, e) 20 | } 21 | } 22 | } 23 | 24 | public func commitBinder( 25 | name: String = "", 26 | description: String = "", 27 | file: StaticString = #file, 28 | function: StaticString = #function, 29 | line: UInt = #line, 30 | mutate: @escaping (inout State, S?) -> Void 31 | ) -> Binder { 32 | 33 | return Binder(self) { t, e in 34 | t.commit(name, description, file, function, line) { s in 35 | mutate(&s, e) 36 | } 37 | } 38 | } 39 | 40 | public func commitBinder( 41 | name: String = "", 42 | description: String = "", 43 | target: WritableKeyPath, 44 | file: StaticString = #file, 45 | function: StaticString = #function, 46 | line: UInt = #line 47 | ) -> Binder { 48 | 49 | return Binder(self) { t, e in 50 | t.commit(name, description, file, function, line) { s in 51 | s[keyPath: target] = e 52 | } 53 | } 54 | } 55 | 56 | public func commitBinder( 57 | name: String = "", 58 | description: String = "", 59 | target: WritableKeyPath, 60 | file: StaticString = #file, 61 | function: StaticString = #function, 62 | line: UInt = #line 63 | ) -> Binder { 64 | 65 | return Binder(self) { t, e in 66 | t.commit(name, description, file, function, line) { s in 67 | s[keyPath: target] = e 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Verge.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | //! Project version number for CycleViewModel. 5 | FOUNDATION_EXPORT double VergeVersionNumber; 6 | 7 | //! Project version string for CycleViewModel. 8 | FOUNDATION_EXPORT const unsigned char VergeVersionString[]; 9 | 10 | // In this header, you should import all the public headers of your framework using statements like #import 11 | 12 | 13 | -------------------------------------------------------------------------------- /Sources/VergeClassic/Verge@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VergeGroup/swift-verge/61e8909d36a430a6fcaba96dd8dc7a9bf260771f/Sources/VergeClassic/Verge@2x.png -------------------------------------------------------------------------------- /Sources/VergeClassic/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VergeGroup/swift-verge/61e8909d36a430a6fcaba96dd8dc7a9bf260771f/Sources/VergeClassic/demo.gif -------------------------------------------------------------------------------- /Sources/VergeMacros/Source.swift: -------------------------------------------------------------------------------- 1 | 2 | @freestanding(expression) 3 | public macro keyPathMap(_ keyPaths: repeat KeyPath) -> (borrowing U) -> (repeat each T) = #externalMacro(module: "VergeMacrosPlugin", type: "KeyPathMap") 4 | -------------------------------------------------------------------------------- /Sources/VergeMacrosPlugin/KeyPathMap.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | 5 | public struct KeyPathMap: Macro { 6 | 7 | 8 | } 9 | 10 | extension KeyPathMap: ExpressionMacro { 11 | 12 | public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax { 13 | 14 | let keyPahts = node.argumentList 15 | 16 | let names: [(String, KeyPathComponentListSyntax)] = { 17 | return keyPahts.map { keyPath in 18 | let components = keyPath.cast(LabeledExprSyntax.self).expression.cast( 19 | KeyPathExprSyntax.self 20 | ).components 21 | 22 | let name = 23 | components 24 | .map { 25 | $0.cast(KeyPathComponentSyntax.self).component.cast( 26 | KeyPathPropertyComponentSyntax.self 27 | ).declName.baseName.description 28 | } 29 | .joined(separator: "_") 30 | 31 | return (name, components) 32 | } 33 | }() 34 | 35 | let line = names.map { arg in 36 | "$0\(arg.1)" 37 | } 38 | .joined(separator: ", ") 39 | 40 | return "{ (\(raw: line)) }" 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/VergeMacrosPlugin/MacroError.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftDiagnostics 3 | 4 | public struct MacroError: Error, DiagnosticMessage { 5 | 6 | public var message: String 7 | 8 | public var diagnosticID: SwiftDiagnostics.MessageID { 9 | .init(domain: "Verge", id: "MacroError") 10 | } 11 | 12 | public var severity: SwiftDiagnostics.DiagnosticSeverity = .error 13 | 14 | init(message: String) { 15 | self.message = message 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VergeMacrosPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | @main 7 | struct Plugin: CompilerPlugin { 8 | let providingMacros: [Macro.Type] = [ 9 | KeyPathMap.self, 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/VergeNormalizationDerived/DerivedResult.swift: -------------------------------------------------------------------------------- 1 | import Verge 2 | 3 | /// A result instance that contains created Derived object 4 | /// While creating non-null derived from entity id, some entity may be not founded. 5 | /// Created derived object are stored in hashed storage to the consumer can check if the entity was not found by the id. 6 | public struct DerivedResult { 7 | 8 | /// A dictionary of Derived that stored by id 9 | /// It's faster than filtering values array to use this dictionary to find missing id or created id. 10 | public private(set) var storage: [Entity.TypedID : Derived] = [:] 11 | 12 | /// An array of Derived that orderd by specified the order of id. 13 | public private(set) var values: [Derived] 14 | 15 | public init() { 16 | self.storage = [:] 17 | self.values = [] 18 | } 19 | 20 | public mutating func append(derived: Derived, id: Entity.TypedID) { 21 | storage[id] = derived 22 | values.append(derived) 23 | } 24 | 25 | } 26 | 27 | public typealias NonNullDerivedResult = DerivedResult 28 | -------------------------------------------------------------------------------- /Sources/VergeNormalizationDerived/EntityType+Typealias.swift: -------------------------------------------------------------------------------- 1 | extension EntityType { 2 | 3 | public typealias Derived = Verge.Derived> 4 | public typealias NonNullDerived = Verge.Derived> 5 | 6 | } 7 | 8 | extension Derived where Value: NonNullEntityWrapperType { 9 | 10 | public var entityID: Value.Entity.TypedID { 11 | self.state.id 12 | } 13 | } 14 | 15 | extension Derived where Value: EntityWrapperType { 16 | 17 | public var entityID: Value.Entity.TypedID? { 18 | self.state.id 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/VergeNormalizationDerived/EntityWrapper.swift: -------------------------------------------------------------------------------- 1 | import Normalization 2 | import StateStruct 3 | 4 | public protocol EntityWrapperType: TrackingObject { 5 | associatedtype Entity: EntityType 6 | 7 | var id: Entity.TypedID { get } 8 | } 9 | 10 | /// A value that wraps an entity and results of fetching. 11 | @Tracking 12 | public struct EntityWrapper: Sendable, EntityWrapperType { 13 | 14 | public let wrapped: Entity? 15 | 16 | public let id: Entity.TypedID 17 | 18 | public init(id: Entity.TypedID, entity: Entity?) { 19 | self.id = id 20 | self.wrapped = entity 21 | } 22 | 23 | } 24 | 25 | extension EntityWrapper: Equatable where Entity: Equatable { 26 | 27 | } 28 | 29 | extension EntityWrapper: Hashable where Entity: Hashable { 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Sources/VergeNormalizationDerived/NonNullEntityWrapper.swift: -------------------------------------------------------------------------------- 1 | import Normalization 2 | import StateStruct 3 | 4 | public protocol NonNullEntityWrapperType: TrackingObject { 5 | associatedtype Entity: EntityType 6 | 7 | var id: Entity.TypedID { get } 8 | } 9 | 10 | /// A value that wraps an entity and results of fetching. 11 | @dynamicMemberLookup 12 | @Tracking 13 | public struct NonNullEntityWrapper: Sendable, NonNullEntityWrapperType { 14 | 15 | /// An entity value 16 | public let wrapped: Entity 17 | 18 | /// An identifier 19 | public let id: Entity.TypedID 20 | 21 | @available(*, deprecated, renamed: "isFallBack") 22 | public var isUsingFallback: Bool { 23 | isFallBack 24 | } 25 | 26 | /// A boolean value that indicates whether the wrapped entity is last value and has been removed from source store. 27 | public let isFallBack: Bool 28 | 29 | public init(entity: Entity, isFallBack: Bool) { 30 | self.id = entity.entityID 31 | self.wrapped = entity 32 | self.isFallBack = isFallBack 33 | } 34 | 35 | public subscript(dynamicMember keyPath: KeyPath) -> Property { 36 | wrapped[keyPath: keyPath] 37 | } 38 | 39 | } 40 | 41 | extension NonNullEntityWrapper: Equatable where Entity: Equatable {} 42 | 43 | extension NonNullEntityWrapper: Hashable where Entity: Hashable {} 44 | 45 | -------------------------------------------------------------------------------- /Sources/VergeNormalizationDerived/StoreType+.swift: -------------------------------------------------------------------------------- 1 | import Verge 2 | import Normalization 3 | 4 | extension StoreType { 5 | 6 | public func normalizedStorage(_ selector: Selector) -> NormalizedStoragePath { 7 | .init(store: self, storageSelector: selector) 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift: -------------------------------------------------------------------------------- 1 | @_exported import Verge 2 | @_exported import Normalization 3 | -------------------------------------------------------------------------------- /Sources/VergeRx/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Hiroshi Kimura(Muukii) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #if !COCOAPODS 23 | import Verge 24 | import Combine 25 | #endif 26 | 27 | import RxSwift 28 | 29 | extension VergeAnyCancellable: RxSwift.Disposable { 30 | public func dispose() { 31 | cancel() 32 | } 33 | } 34 | 35 | extension Publisher { 36 | /// Returns an Observable representing the underlying 37 | /// Publisher. Upon subscription, the Publisher's sink pushes 38 | /// events into the Observable. Upon disposing of the subscription, 39 | /// the sink is cancelled. 40 | /// 41 | /// - returns: Observable 42 | func asObservable() -> Observable { 43 | Observable.create { observer in 44 | let cancellable = self.sink( 45 | receiveCompletion: { completion in 46 | switch completion { 47 | case .finished: 48 | observer.onCompleted() 49 | case .failure(let error): 50 | observer.onError(error) 51 | } 52 | }, 53 | receiveValue: { value in 54 | observer.onNext(value) 55 | }) 56 | 57 | return Disposables.create { cancellable.cancel() } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/VergeRx/Store+Rx.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | #if !COCOAPODS 5 | @_spi(Package) import Verge 6 | @_spi(EventEmitter) import Verge 7 | #endif 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | 13 | extension Store: ReactiveCompatible {} 14 | 15 | extension Reactive where Base : StoreDriverType { 16 | 17 | /// An observable that repeatedly emits the changes when state updated 18 | /// 19 | /// Guarantees to emit the first event on started subscribing. 20 | /// 21 | /// - Parameter startsFromInitial: Make the first changes object's hasChanges always return true. 22 | /// - Returns: 23 | public func stateObservable() -> Observable> { 24 | base.store.asStore()._statePublisher().asObservable() 25 | } 26 | 27 | public func activitySignal() -> Signal { 28 | base.store.asStore()._activityPublisher().asObservable().asSignal(onErrorRecover: { _ in Signal.empty() }) 29 | } 30 | 31 | } 32 | 33 | extension StoreDriverType { 34 | 35 | @_disfavoredOverload 36 | public var rx: Reactive { 37 | return .init(self) 38 | } 39 | 40 | } 41 | 42 | extension ObservableType where Element : ChangesType, Element.Value : Equatable { 43 | 44 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 45 | /// 46 | /// Using Changes under 47 | /// 48 | /// - Parameters: 49 | /// - selector: 50 | /// - compare: 51 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 52 | public func changed() -> Observable { 53 | changed({ $0 }) 54 | } 55 | 56 | } 57 | 58 | extension ObservableType where Element : ChangesType { 59 | 60 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 61 | /// 62 | /// Using Changes under 63 | /// 64 | /// - Parameters: 65 | /// - selector: 66 | /// - compare: 67 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 68 | public func changed(_ selector: @escaping (Element.Value) -> S, _ comparer: some TypedComparator) -> Observable { 69 | 70 | return flatMap { changes -> Observable in 71 | let _r = changes.asChanges().ifChanged(selector, comparer) { value in 72 | return value 73 | } 74 | return _r.map { .just($0) } ?? .empty() 75 | } 76 | } 77 | 78 | /// Returns an observable sequence that contains only changed elements according to the `comparer`. 79 | /// 80 | /// Using Changes under 81 | /// 82 | /// - Parameters: 83 | /// - selector: 84 | /// - comparer: 85 | /// - Returns: Returns an observable sequence that contains only changed elements according to the `comparer`. 86 | public func changed(_ selector: @escaping (Element.Value) -> S) -> Observable { 87 | return changed(selector, EqualityComparator()) 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "rxswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/ReactiveX/RxSwift.git", 7 | "state" : { 8 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", 9 | "version" : "6.5.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-atomics", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-atomics.git", 16 | "state" : { 17 | "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", 18 | "version" : "1.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-collections", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-collections", 25 | "state" : { 26 | "branch" : "main", 27 | "revision" : "939cfd25234472b4dc91c3caeab304d15bca9a73" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-docc-plugin", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-docc-plugin.git", 34 | "state" : { 35 | "branch" : "main", 36 | "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-docc-symbolkit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-docc-symbolkit", 43 | "state" : { 44 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 45 | "version" : "1.0.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swiftui-hosting", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/FluidGroup/swiftui-hosting.git", 52 | "state" : { 53 | "revision" : "064204fab9a34aeb267b2418e1037006d043c147", 54 | "version" : "1.1.0" 55 | } 56 | }, 57 | { 58 | "identity" : "viewinspector", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/nalexn/ViewInspector.git", 61 | "state" : { 62 | "revision" : "e42529aa0b6ff57393ab52540f6006f3a709ee00", 63 | "version" : "0.9.6" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo/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 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // StoreReaderDemo 4 | // 5 | // Created by Muukii on 2023/05/11. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIHosting 10 | import Verge 11 | 12 | struct ContentView: View { 13 | var body: some View { 14 | VStack { 15 | Image(systemName: "globe") 16 | .imageScale(.large) 17 | .foregroundColor(.accentColor) 18 | Text("Hello, world!") 19 | } 20 | .padding() 21 | } 22 | } 23 | 24 | struct ContentView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | ContentView() 27 | } 28 | } 29 | 30 | enum Preview_StoreReader: PreviewProvider { 31 | 32 | static var previews: some View { 33 | 34 | Group { 35 | 36 | ViewHosting().previewDisplayName("ViewHosting") 37 | 38 | } 39 | 40 | } 41 | 42 | struct ViewHosting: UIViewRepresentable { 43 | 44 | func makeUIView(context: Context) -> UIView { 45 | _View() 46 | } 47 | 48 | func updateUIView(_ uiView: UIView, context: Context) { 49 | 50 | } 51 | } 52 | 53 | struct BindingView: View { 54 | 55 | @Binding var value: String 56 | 57 | init(value: Binding) { 58 | self._value = value 59 | } 60 | 61 | var body: some View { 62 | VStack { 63 | Text(value) 64 | Button("Up") { 65 | value += "A" 66 | } 67 | } 68 | } 69 | } 70 | 71 | final class _View: UIView { 72 | 73 | init() { 74 | super.init(frame: .null) 75 | 76 | @UIState var value: String = "BBB" 77 | 78 | let view = SwiftUIHostingView { 79 | StoreReader($value) { valueProxy in 80 | BindingView( 81 | value: valueProxy.binding(\.self) 82 | ) 83 | } 84 | } 85 | 86 | addSubview(view) 87 | view.frame = bounds 88 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 89 | 90 | } 91 | 92 | required init?(coder: NSCoder) { 93 | fatalError("init(coder:) has not been implemented") 94 | } 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StoreReaderDemo/StoreReaderDemo/StoreReaderDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreReaderDemoApp.swift 3 | // StoreReaderDemo 4 | // 5 | // Created by Muukii on 2023/05/11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct StoreReaderDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "rxswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/ReactiveX/RxSwift.git", 7 | "state" : { 8 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", 9 | "version" : "6.5.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-atomics", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-atomics.git", 16 | "state" : { 17 | "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", 18 | "version" : "1.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-collections", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-collections", 25 | "state" : { 26 | "branch" : "main", 27 | "revision" : "53a8adc54374f620002a3b6401d39e0feb3c57ae" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-docc-plugin", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-docc-plugin.git", 34 | "state" : { 35 | "branch" : "main", 36 | "revision" : "106fd09cc69dfa610cb12770f6aee59671aed109" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-docc-symbolkit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-docc-symbolkit", 43 | "state" : { 44 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 45 | "version" : "1.0.0" 46 | } 47 | }, 48 | { 49 | "identity" : "viewinspector", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/nalexn/ViewInspector.git", 52 | "state" : { 53 | "revision" : "4effbd9143ab797eb60d2f32d4265c844c980946", 54 | "version" : "0.9.5" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/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 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/Book.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Verge 3 | import VergeTaskManager 4 | 5 | struct BookVergeTaskManager: View, PreviewProvider { 6 | var body: some View { 7 | Content() 8 | } 9 | 10 | static var previews: some View { 11 | Self() 12 | } 13 | 14 | private struct Content: View { 15 | 16 | @StateObject var viewModel: ViewModel = .init() 17 | @State var count = 0 18 | 19 | var body: some View { 20 | VStack { 21 | 22 | Button("Wait") { 23 | count += 1 24 | viewModel.fetch(token: count.description) 25 | } 26 | 27 | Button("Override") { 28 | count += 1 29 | viewModel.fetchOverride(token: count.description) 30 | } 31 | 32 | Button("Cancel All") { 33 | viewModel.cancelAll() 34 | } 35 | 36 | } 37 | } 38 | } 39 | 40 | final class ViewModel: StoreComponentType, ObservableObject { 41 | 42 | struct State: Equatable { 43 | 44 | } 45 | 46 | let store = Store(initialState: .init()) 47 | 48 | init() { 49 | 50 | } 51 | 52 | enum _Key: TaskKeyType { 53 | 54 | } 55 | 56 | func cancelAll() { 57 | Task { 58 | await store.taskManager.cancelAll() 59 | } 60 | } 61 | 62 | func fetchOverride(token: String) { 63 | Task { 64 | let ref = await store.taskManager.task( 65 | label: token, 66 | key: .init(_Key.self), 67 | mode: .dropCurrent 68 | ) { 69 | await networking(token: token) 70 | } 71 | 72 | let r = Resource(name: token) 73 | 74 | Task { 75 | print("-> Ref", token) 76 | let _ = try? await ref.value 77 | print("<- Ref", token) 78 | withExtendedLifetime(r) {} 79 | } 80 | } 81 | } 82 | 83 | func fetch(token: String) { 84 | Task { 85 | let ref = await store.taskManager.task( 86 | label: token, 87 | key: .init(_Key.self), 88 | mode: .waitInCurrent 89 | ) { 90 | await networking(token: token) 91 | } 92 | 93 | let r = Resource(name: token) 94 | 95 | Task { 96 | print("-> Ref", token) 97 | let _ = try? await ref.value 98 | print("<- Ref", token) 99 | withExtendedLifetime(r) {} 100 | } 101 | } 102 | } 103 | 104 | } 105 | 106 | } 107 | 108 | private final class Resource { 109 | 110 | private let name: String 111 | 112 | init(name: String) { 113 | self.name = name 114 | } 115 | 116 | deinit { 117 | print("Deinit", name) 118 | } 119 | } 120 | 121 | private func networking(token: String) async { 122 | print("✈️ Start", token) 123 | try? await Task.sleep(nanoseconds: 2_000_000_000) 124 | if Task.isCancelled { 125 | print("❌ Cancelled", token) 126 | } else { 127 | print("✅ Done", token) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // TaskManagerPlayground 4 | // 5 | // Created by Muukii on 2023/04/07. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | BookVergeTaskManager() 13 | } 14 | } 15 | 16 | struct ContentView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | ContentView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TaskManagerPlayground/TaskManagerPlayground/TaskManagerPlaygroundApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskManagerPlaygroundApp.swift 3 | // TaskManagerPlayground 4 | // 5 | // Created by Muukii on 2023/04/07. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TaskManagerPlaygroundApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/All.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "CED3C5CF-7541-4D76-8BD7-C398F4B5517C", 5 | "name" : "Basic", 6 | "options" : { 7 | 8 | } 9 | }, 10 | { 11 | "id" : "C8A4A8E7-7082-47B7-8C0E-8D4DC5BF01E2", 12 | "name" : "Thread Sanitization", 13 | "options" : { 14 | "threadSanitizerEnabled" : true, 15 | "undefinedBehaviorSanitizerEnabled" : true 16 | } 17 | }, 18 | { 19 | "id" : "1C451F14-8D88-40D3-949E-08EC2939DA3D", 20 | "name" : "Address Sanitization", 21 | "options" : { 22 | "addressSanitizer" : { 23 | "detectStackUseAfterReturn" : true, 24 | "enabled" : true 25 | }, 26 | "undefinedBehaviorSanitizerEnabled" : true 27 | } 28 | } 29 | ], 30 | "defaultOptions" : { 31 | 32 | }, 33 | "testTargets" : [ 34 | { 35 | "parallelizable" : true, 36 | "target" : { 37 | "containerPath" : "container:Verge.xcodeproj", 38 | "identifier" : "4B475BC4239ADE1C008A03E1", 39 | "name" : "VergeORMTests" 40 | } 41 | }, 42 | { 43 | "parallelizable" : true, 44 | "skippedTests" : [ 45 | "ReproduceDeadlockTests\/testReproduceDeadlock()" 46 | ], 47 | "target" : { 48 | "containerPath" : "container:Verge.xcodeproj", 49 | "identifier" : "4B1743A623C767670074C457", 50 | "name" : "VergeRxTests" 51 | } 52 | }, 53 | { 54 | "parallelizable" : true, 55 | "skippedTests" : [ 56 | "GetterTests\/testChain()", 57 | "VergeStoreTests\/testOrderOfEvents()" 58 | ], 59 | "target" : { 60 | "containerPath" : "container:Verge.xcodeproj", 61 | "identifier" : "4B68394323705ACD002FFC5A", 62 | "name" : "VergeTests" 63 | } 64 | }, 65 | { 66 | "parallelizable" : true, 67 | "target" : { 68 | "containerPath" : "container:Verge.xcodeproj", 69 | "identifier" : "4B65F8EB1FB4C0DC00A90A67", 70 | "name" : "VergeClassicTests" 71 | } 72 | } 73 | ], 74 | "version" : 1 75 | } 76 | -------------------------------------------------------------------------------- /Tests/DemoState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoState.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2020/04/21. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Verge 11 | import XCTest 12 | import Observation 13 | 14 | struct NonEquatable: Sendable { 15 | let id = UUID() 16 | } 17 | struct OnEquatable: Equatable, Sendable { 18 | let id = UUID() 19 | } 20 | 21 | struct DemoState: StateType, Sendable { 22 | 23 | struct Inner: Equatable { 24 | var name: String = "" 25 | } 26 | 27 | var name: String = "" 28 | var count: Int = 0 29 | var items: [Int] = [] 30 | var inner: Inner = .init() 31 | 32 | @Edge var nonEquatable: NonEquatable = .init() 33 | 34 | @Edge var onEquatable: OnEquatable = .init() 35 | 36 | mutating func updateFromItself() { 37 | count += 1 38 | } 39 | 40 | } 41 | 42 | enum DemoActivity { 43 | case something 44 | } 45 | 46 | #if canImport(Verge) 47 | 48 | import Verge 49 | 50 | final class DemoStore: Verge.Store { 51 | 52 | init() { 53 | super.init(initialState: .init(), logger: nil) 54 | } 55 | 56 | func increment() { 57 | commit { 58 | $0.count += 1 59 | } 60 | } 61 | 62 | func empty() { 63 | commit { _ in 64 | } 65 | } 66 | 67 | } 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /Tests/VergeMacrosTests/KeyPathMapTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntaxMacros 2 | import SwiftSyntaxMacrosTestSupport 3 | import XCTest 4 | 5 | #if canImport(VergeMacrosPlugin) 6 | import VergeMacrosPlugin 7 | 8 | final class KeyPathMapTests: XCTestCase { 9 | 10 | func test_() { 11 | 12 | assertMacroExpansion( 13 | #""" 14 | #keyPathMap(\.foo) 15 | """#, 16 | expandedSource: #""" 17 | { 18 | ($0.foo) 19 | } 20 | """#, 21 | macros: ["keyPathMap": KeyPathMap.self] 22 | ) 23 | 24 | assertMacroExpansion( 25 | #""" 26 | #keyPathMap(\.foo, \.aaa) 27 | """#, 28 | expandedSource: #""" 29 | { 30 | ($0.foo, $0.aaa) 31 | } 32 | """#, 33 | macros: ["keyPathMap": KeyPathMap.self] 34 | ) 35 | 36 | assertMacroExpansion( 37 | #""" 38 | #keyPathMap(\State.foo, \.aaa) 39 | """#, 40 | expandedSource: #""" 41 | { 42 | ($0.foo, $0.aaa) 43 | } 44 | """#, 45 | macros: ["keyPathMap": KeyPathMap.self] 46 | ) 47 | } 48 | 49 | } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /Tests/VergeNormalizationDerivedTests/CombiningTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import VergeNormalizationDerived 3 | 4 | final class CombiningTests: XCTestCase { 5 | 6 | func test_combine() { 7 | 8 | let store = Store( 9 | initialState: .init() 10 | ) 11 | 12 | struct View: Equatable { 13 | var book: Book? 14 | var author: Author? 15 | } 16 | 17 | let derived = store.normalizedStorage(.keyPath(\.db)).derived { storage in 18 | View( 19 | book: storage.book.find(by: .init("1")), 20 | author: storage.author.find(by: .init("1")) 21 | ) 22 | } 23 | 24 | let exp = expectation(description: "call") 25 | exp.expectedFulfillmentCount = 3 26 | 27 | let sub = derived.sinkState { view in 28 | exp.fulfill() 29 | } 30 | 31 | store.commit { 32 | $0.db.performBatchUpdates { t in 33 | t.modifying.book.insert(Book(rawID: "1", authorID: .init("1"))) 34 | t.modifying.author.insert(Author(rawID: "1", name: "Hiroshi")) 35 | } 36 | } 37 | 38 | // no affects 39 | store.commit { 40 | $0.count += 1 41 | } 42 | 43 | _ = store.commit { 44 | $0.db.performBatchUpdates { t in 45 | t.modifying.author.insert(Author(rawID: "1", name: "Hiroshi Kimura")) 46 | } 47 | } 48 | 49 | wait(for: [exp], timeout: 10) 50 | 51 | XCTAssertEqual(derived.state.author?.name, "Hiroshi Kimura") 52 | 53 | _ = sub 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Tests/VergeNormalizationDerivedTests/DemoState.swift: -------------------------------------------------------------------------------- 1 | import VergeNormalizationDerived 2 | 3 | @Tracking 4 | struct DemoState { 5 | 6 | var count: Int = 0 7 | 8 | var db: Database = .init() 9 | 10 | struct DatabaseSelector: StorageSelector { 11 | typealias Source = DemoState 12 | typealias Storage = Database 13 | 14 | func select(source: consuming DemoState) -> Database { 15 | source.db 16 | } 17 | } 18 | } 19 | 20 | extension StorageSelector where Self == DemoState.DatabaseSelector { 21 | static var db: Self { 22 | DemoState.DatabaseSelector() 23 | } 24 | } 25 | 26 | @NormalizedStorage 27 | struct Database { 28 | 29 | @Table 30 | var book: Tables.Hash = .init() 31 | 32 | @Table 33 | var book2: Tables.Hash = .init() 34 | 35 | @Table 36 | var author: Tables.Hash = .init() 37 | 38 | } 39 | 40 | extension Database { 41 | 42 | } 43 | 44 | struct Book: EntityType, Hashable { 45 | 46 | typealias TypedIdentifierRawValue = String 47 | 48 | var typedID: TypedID { 49 | .init(rawID) 50 | } 51 | 52 | let rawID: String 53 | let authorID: Author.TypedID 54 | var name: String = "initial" 55 | } 56 | 57 | struct Author: EntityType { 58 | 59 | typealias TypedIdentifierRawValue = String 60 | 61 | var typedID: TypedID { 62 | .init(rawID) 63 | } 64 | 65 | let rawID: String 66 | var name: String = "" 67 | 68 | static let anonymous: Author = .init(rawID: "anonymous") 69 | } 70 | -------------------------------------------------------------------------------- /Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift: -------------------------------------------------------------------------------- 1 | import VergeNormalizationDerived 2 | import XCTest 3 | 4 | extension Store where State == DemoState { 5 | 6 | var database: NormalizedStoragePath { 7 | return .init(store: self, storageSelector: .init()) 8 | } 9 | 10 | } 11 | 12 | final class VergeNormalizationDerivedTests: XCTestCase { 13 | 14 | func test_subscribe() { 15 | 16 | let exp = expectation(description: "wait") 17 | 18 | let store = Store( 19 | initialState: .init() 20 | ) 21 | 22 | let derived = store 23 | .normalizedStorage(.keyPath(\.db)) 24 | .table(.keyPath(\.book)) 25 | .derived(from: Book.TypedID.init("1")) 26 | 27 | var received: [Book?] = [] 28 | 29 | derived.sinkState { value in 30 | received.append(value.primitive.wrapped) 31 | if value.primitive.wrapped != nil { 32 | exp.fulfill() 33 | } 34 | } 35 | .storeWhileSourceActive() 36 | 37 | _ = store.commit { 38 | $0.db.performBatchUpdates { t in 39 | t.modifying.book.insert(.init(rawID: "1", authorID: .init("muukii"))) 40 | } 41 | } 42 | 43 | wait(for: [exp]) 44 | 45 | XCTAssertEqual(received, [nil, .init(rawID: "1", authorID: .init("muukii"))]) 46 | 47 | withExtendedLifetime(derived, {}) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Tests/VergeRxTests/ChangedOperatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangedOperatorTests.swift 3 | // VergeRxTests 4 | // 5 | // Created by muukii on 2020/05/01. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | 12 | import XCTest 13 | 14 | import VergeRx 15 | import Verge 16 | 17 | class ChangedOperatorTests: XCTestCase { 18 | 19 | let store = Store.init(initialState: .init(), logger: nil) 20 | 21 | func testChanged() { 22 | 23 | 24 | let count = store.derived(.map(\.count)) 25 | 26 | let exp = expectation(description: "") 27 | exp.assertForOverFulfill = true 28 | exp.expectedFulfillmentCount = 3 29 | 30 | _ = count.rx.stateObservable() 31 | .changed({ $0.description }) 32 | .subscribe(onNext: { _ in 33 | exp.fulfill() 34 | }) 35 | 36 | store.commit { 37 | $0.count += 1 38 | } 39 | store.commit { 40 | $0.count += 1 41 | } 42 | store.commit { _ in 43 | 44 | } 45 | wait(for: [exp], timeout: 2) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/VergeRxTests/DemoState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoState.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2020/04/21. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Verge 11 | import XCTest 12 | import Observation 13 | 14 | struct NonEquatable: Sendable { 15 | let id = UUID() 16 | } 17 | struct OnEquatable: Equatable, Sendable { 18 | let id = UUID() 19 | } 20 | 21 | @Tracking 22 | struct DemoState: Sendable { 23 | 24 | struct Inner: Equatable { 25 | var name: String = "" 26 | } 27 | 28 | var name: String = "" 29 | var count: Int = 0 30 | var items: [Int] = [] 31 | var inner: Inner = .init() 32 | 33 | var nonEquatable: NonEquatable = .init() 34 | 35 | var onEquatable: OnEquatable = .init() 36 | 37 | mutating func updateFromItself() { 38 | count += 1 39 | } 40 | 41 | } 42 | 43 | enum DemoActivity { 44 | case something 45 | } 46 | 47 | #if canImport(Verge) 48 | 49 | import Verge 50 | 51 | final class DemoStore: Verge.Store { 52 | 53 | init() { 54 | super.init(initialState: .init(), logger: nil) 55 | } 56 | 57 | func increment() { 58 | commit { 59 | $0.count += 1 60 | } 61 | } 62 | 63 | func empty() { 64 | commit { _ in 65 | } 66 | } 67 | 68 | } 69 | 70 | #endif 71 | -------------------------------------------------------------------------------- /Tests/VergeRxTests/ReproduceDeadlockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReproduceDeadlockTests.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2020/04/20. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | import Verge 13 | import VergeRx 14 | 15 | class ReproduceDeadlockTests: XCTestCase { 16 | 17 | final class StoreWrapper: StoreDriverType, Sendable { 18 | 19 | struct State: Equatable { 20 | var count = 0 21 | } 22 | 23 | let store = Store.init(initialState: .init(), logger: nil) 24 | 25 | init() { 26 | } 27 | } 28 | 29 | func testReproduceDeadlock() { 30 | 31 | let store = StoreWrapper() 32 | 33 | _ = store.rx.stateObservable().bind { state in 34 | if state.count == 1 { 35 | let group = DispatchGroup() 36 | group.enter() 37 | DispatchQueue.global().async { 38 | store.commit { $0.count += 1 } 39 | group.leave() 40 | } 41 | group.wait() 42 | } 43 | } 44 | 45 | store.commit { 46 | $0.count += 1 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/VergeRxTests/SubjectCompletionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubjectCompletionTests.swift 3 | // VergeRxTests 4 | // 5 | // Created by Muukii on 2021/04/06. 6 | // Copyright © 2021 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import Verge 12 | import VergeRx 13 | import XCTest 14 | 15 | final class SubjectCompletionTests: XCTestCase { 16 | 17 | func testStateObsevableCompletion() { 18 | 19 | var subscription: Disposable? 20 | 21 | var store: DemoStore? = DemoStore() 22 | weak var weakStore: DemoStore? = store 23 | 24 | subscription = store?.rx.stateObservable() 25 | .subscribe() 26 | 27 | XCTAssertNotNil(weakStore) 28 | 29 | store = nil 30 | 31 | subscription?.dispose() 32 | 33 | XCTAssertNil(weakStore) 34 | } 35 | 36 | func testActivityObservableCompletion() { 37 | 38 | var subscription: Disposable? 39 | 40 | var store: DemoStore? = DemoStore() 41 | weak var weakStore: DemoStore? = store 42 | 43 | subscription = store?.rx.activitySignal() 44 | .emit() 45 | 46 | XCTAssertNotNil(weakStore) 47 | 48 | store = nil 49 | 50 | subscription?.dispose() 51 | 52 | XCTAssertNil(weakStore) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Tests/VergeRxTests/VergeRxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VergeRxTests.swift 3 | // VergeRxTests 4 | // 5 | // Created by muukii on 2020/01/09. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | import VergeRx 12 | 13 | class VergeRxTests: XCTestCase { 14 | 15 | @MainActor 16 | func testChangesObbservable() { 17 | 18 | let store = DemoStore() 19 | 20 | XCTContext.runActivity(named: "") { (activity) in 21 | 22 | let exp1 = expectation(description: "") 23 | 24 | _ = store.rx.stateObservable() 25 | .subscribe(onNext: { changes in 26 | exp1.fulfill() 27 | XCTAssertEqual(changes.hasChanges(\.count), true) 28 | }) 29 | 30 | XCTAssertEqual(exp1.expectedFulfillmentCount, 1) 31 | 32 | wait(for: [exp1], timeout: 1) 33 | 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/VergeTests/ActivityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTests.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2019/12/24. 6 | // Copyright © 2019 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | import Verge 13 | 14 | #if canImport(Combine) 15 | 16 | import Combine 17 | 18 | @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) 19 | class ActivityTests: XCTestCase { 20 | 21 | struct Value: Equatable { 22 | 23 | } 24 | 25 | enum Activity { 26 | case didSendMessage 27 | } 28 | 29 | final class Store: Verge.Store, @unchecked Sendable { 30 | 31 | init() { 32 | super.init(initialState: .init(), logger: DefaultStoreLogger.default) 33 | } 34 | 35 | func sendMessage() { 36 | send(.didSendMessage) 37 | } 38 | } 39 | 40 | private var subscriptions = Set() 41 | 42 | func testSend() { 43 | 44 | let store = Store() 45 | 46 | let waiter = XCTestExpectation() 47 | waiter.expectedFulfillmentCount = 1 48 | 49 | store 50 | .activityPublisher() 51 | .sink { event in 52 | XCTAssertEqual(event, .didSendMessage) 53 | waiter.fulfill() 54 | } 55 | .store(in: &subscriptions) 56 | 57 | store.sendMessage() 58 | 59 | wait(for: [waiter], timeout: 10) 60 | 61 | } 62 | } 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /Tests/VergeTests/BindingDerivedTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Verge 3 | 4 | final class BindingDerivedTests: XCTestCase { 5 | 6 | func testBinding() { 7 | 8 | let source = DemoStore() 9 | 10 | let binding = source.bindingDerived( 11 | get: .select(\.count), 12 | set: { source, new in 13 | source.count = new 14 | }) 15 | 16 | XCTAssertEqual(binding.state.previous?.primitive, nil) 17 | XCTAssertEqual(binding.state.primitive, 0) 18 | 19 | binding.wrappedValue = 2 20 | 21 | XCTAssertEqual(binding.state.previous?.primitive, 0) 22 | XCTAssertEqual(binding.state.primitive, 2) 23 | XCTAssertEqual(source.state.primitive.count, 2) 24 | 25 | } 26 | 27 | func testBinding_abstract() { 28 | 29 | let source = DemoStore() 30 | 31 | let binding: some StoreDriverType = source.bindingDerived( 32 | get: .select(\.count), 33 | set: { source, new in 34 | source.count = new 35 | }) 36 | 37 | XCTAssertEqual(binding.state.previous?.primitive, nil) 38 | XCTAssertEqual(binding.state.primitive, 0) 39 | 40 | binding.commit { 41 | $0 = 2 42 | } 43 | 44 | XCTAssertEqual(binding.state.previous?.primitive, 0) 45 | XCTAssertEqual(binding.state.primitive, 2) 46 | 47 | XCTAssertEqual(source.state.primitive.count, 2) 48 | 49 | source.commit { 50 | $0.count += 1 51 | } 52 | 53 | XCTAssertEqual(binding.state.previous?.primitive, 2) 54 | XCTAssertEqual(binding.state.primitive, 3) 55 | 56 | } 57 | 58 | func testBinding_upstreamChanged() { 59 | let source = DemoStore() 60 | 61 | let binding = source.bindingDerived( 62 | get: .select(\.count), 63 | set: { source, new in 64 | source.count = new 65 | }) 66 | 67 | source.commit { 68 | $0.count += 1 69 | } 70 | 71 | XCTAssertEqual(source.state.count, 1) 72 | XCTAssertEqual(source.state.previous?.count, 0) 73 | 74 | XCTAssertEqual(binding.state.previous?.primitive, 0) 75 | XCTAssertEqual(binding.state.primitive, 1) 76 | 77 | 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Tests/VergeTests/CachedMapTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CachedMapTests.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2020/07/25. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | import Verge 13 | 14 | final class CachedMapTests: XCTestCase { 15 | 16 | struct Entity { 17 | let id: String 18 | } 19 | 20 | final class ViewModel: Equatable { 21 | 22 | static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { 23 | lhs === rhs 24 | } 25 | 26 | init(entity: Entity) { 27 | } 28 | } 29 | 30 | func testCacheAvailability() { 31 | 32 | let storage = InstancePool.init(keySelector: \.id) 33 | 34 | let fetchedEntities: [Entity] = (0..<100).map { Entity(id: $0.description) } 35 | 36 | let resultA = fetchedEntities.cachedMap(using: storage, sweepsUnused: true, makeNew: { 37 | ViewModel(entity: $0) 38 | }) 39 | 40 | let resultB = fetchedEntities.cachedMap(using: storage, sweepsUnused: true, makeNew: { 41 | XCTFail() 42 | return ViewModel(entity: $0) 43 | }) 44 | 45 | XCTAssertEqual(resultA, resultB) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Tests/VergeTests/ChangesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Verge 3 | import VergeMacros 4 | 5 | final class ChangesTests: XCTestCase { 6 | 7 | func test_performance_keypath() { 8 | 9 | let changes = Changes.init(old: nil, new: .init()) 10 | 11 | measure { 12 | for _ in 0..<1000 { 13 | changes.ifChanged(\.name).do { _ in 14 | 15 | } 16 | } 17 | } 18 | 19 | } 20 | 21 | func test_performance_macro() { 22 | 23 | let changes = Changes.init(old: nil, new: .init()) 24 | 25 | measure { 26 | for _ in 0..<1000 { 27 | _ = changes.ifChanged(#keyPathMap(\.name)) 28 | } 29 | } 30 | 31 | } 32 | 33 | func test_performance_closure() { 34 | 35 | let changes = Changes.init(old: nil, new: .init()) 36 | 37 | measure { 38 | for _ in 0..<1000 { 39 | _ = changes.ifChanged({ $0.name }) 40 | } 41 | } 42 | 43 | } 44 | 45 | func test_same() { 46 | 47 | let changes = Changes.init(old: .init(), new: .init()) 48 | 49 | changes 50 | .ifChanged(#keyPathMap(\.name)) 51 | .do { arg in 52 | XCTFail() 53 | } 54 | 55 | changes 56 | .ifChanged(#keyPathMap(\.name, \.count)) 57 | .do { arg in 58 | XCTFail() 59 | } 60 | 61 | changes 62 | .ifChanged({ $0.name }) 63 | .do { arg in 64 | XCTFail() 65 | } 66 | 67 | changes 68 | .ifChanged({ ($0.name, $0.count) }) 69 | .do { arg in 70 | XCTFail() 71 | } 72 | 73 | } 74 | 75 | func test_diff() { 76 | 77 | let changes = Changes.init( 78 | old: .init(name: "---"), 79 | new: .init() 80 | ) 81 | 82 | var hit = false 83 | 84 | changes 85 | .ifChanged({ ($0.name, $0.count) }) 86 | .do { arg in 87 | hit = true 88 | } 89 | 90 | XCTAssertTrue(hit) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/VergeTests/ComparerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComparerTests.swift 3 | // VergeTests 4 | // 5 | // Created by Muukii on 2022/05/11. 6 | // Copyright © 2022 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Verge 11 | import XCTest 12 | 13 | @available(iOS 13, *) 14 | final class ComparerTests: XCTestCase { 15 | 16 | func testPerfomance_old() { 17 | 18 | let base = Comparer { $0 == $1 } 19 | 20 | measure(metrics: [XCTMemoryMetric(), XCTCPUMetric()]) { 21 | for _ in 0..<10000 { 22 | _ = base.equals("A", "B") 23 | } 24 | } 25 | } 26 | 27 | func testPerfomance_new() { 28 | 29 | let base = EqualityComparator() 30 | 31 | measure(metrics: [XCTMemoryMetric(), XCTCPUMetric()]) { 32 | for _ in 0..<10000 { 33 | _ = base("A", "B") 34 | } 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Tests/VergeTests/CounterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateTests.swift 3 | // VergeCore 4 | // 5 | // Created by muukii on 2020/01/13. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | 13 | import Verge 14 | 15 | final class CounterTests: XCTestCase { 16 | 17 | func testCounter() { 18 | 19 | var counter = NonAtomicCounter() 20 | 21 | for _ in 0..<100 { 22 | 23 | counter.increment() 24 | } 25 | 26 | XCTAssertEqual(counter.value, 100) 27 | } 28 | 29 | func testCounterPerformance() { 30 | var counter = NonAtomicCounter() 31 | if #available(iOS 13.0, *) { 32 | measure(metrics: [XCTCPUMetric()]) { 33 | counter.increment() 34 | } 35 | } else { 36 | // Fallback on earlier versions 37 | } 38 | } 39 | 40 | func testGenDatePerformance() { 41 | 42 | measure(metrics: [XCTMemoryMetric(), XCTCPUMetric(), XCTClockMetric()]) { 43 | _ = Date() 44 | } 45 | } 46 | 47 | func testGenCFDatePerformance() { 48 | 49 | measure(metrics: [XCTMemoryMetric(), XCTCPUMetric(), XCTClockMetric()]) { 50 | _ = CFAbsoluteTimeGetCurrent() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/VergeTests/DemoState.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 4 | // DemoState.swift 5 | // VergeStoreTests 6 | // 7 | // Created by muukii on 2020/04/21. 8 | // Copyright © 2020 muukii. All rights reserved. 9 | // 10 | 11 | import Foundation 12 | import Verge 13 | import XCTest 14 | import Observation 15 | 16 | struct NonEquatable: Sendable { 17 | let id = UUID() 18 | } 19 | struct OnEquatable: Equatable, Sendable { 20 | let id = UUID() 21 | } 22 | 23 | @Tracking 24 | struct DemoState: Sendable { 25 | 26 | struct Inner: Equatable { 27 | var name: String = "" 28 | } 29 | 30 | var name: String = "" 31 | var count: Int = 0 32 | var items: [Int] = [] 33 | var inner: Inner = .init() 34 | 35 | init() { 36 | 37 | } 38 | 39 | init(name: String) { 40 | self.name = name 41 | } 42 | 43 | init(name: String, count: Int) { 44 | self.name = name 45 | self.count = count 46 | } 47 | 48 | var nonEquatable: NonEquatable = .init() 49 | 50 | var onEquatable: OnEquatable = .init() 51 | 52 | var recursive: DemoState? = nil 53 | 54 | mutating func updateFromItself() { 55 | count += 1 56 | } 57 | 58 | } 59 | 60 | enum DemoActivity { 61 | case something 62 | } 63 | 64 | #if canImport(Verge) 65 | 66 | import Verge 67 | 68 | final class DemoStore: Verge.Store { 69 | 70 | init() { 71 | super.init(initialState: .init(), logger: nil) 72 | } 73 | 74 | func increment() { 75 | commit { 76 | $0.count += 1 77 | } 78 | } 79 | 80 | func empty() { 81 | commit { _ in 82 | } 83 | } 84 | 85 | } 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /Tests/VergeTests/EdgeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeTests.swift 3 | // VergeTests 4 | // 5 | // Created by Muukii on 2020/12/14. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | import Verge 13 | 14 | final class EdgeTests: XCTestCase { 15 | 16 | struct Mock: Equatable { 17 | 18 | static func == (lhs: Self, rhs: Self) -> Bool { 19 | lhs.onCallEquatable() 20 | rhs.onCallEquatable() 21 | return lhs.id == rhs.id 22 | } 23 | 24 | var onCallEquatable: () -> Void = {} 25 | let id = UUID() 26 | } 27 | 28 | func testComparion() { 29 | 30 | let exp = expectation(description: "onCall") 31 | exp.assertForOverFulfill = false 32 | 33 | var mock = Mock() 34 | mock.onCallEquatable = { 35 | exp.fulfill() 36 | } 37 | 38 | let edge = Edge.init(wrappedValue: mock) 39 | var edge2 = edge 40 | 41 | XCTAssertEqual(edge.version, edge2.version) 42 | 43 | edge2.wrappedValue = mock 44 | 45 | XCTAssertNotEqual(edge.version, edge2.version) 46 | 47 | XCTAssertTrue(edge == edge2) 48 | wait(for: [exp], timeout: 1) 49 | } 50 | 51 | func testTuple() { 52 | 53 | let a = Edge.init(wrappedValue: (1, 2)) 54 | let b = Edge.init(wrappedValue: (1, 2)) 55 | 56 | XCTAssertEqual(a, b) 57 | 58 | } 59 | 60 | func testTuple_NoSupports_Equtable() { 61 | 62 | let a = Edge.init(wrappedValue: (1, 2, 3, 4, 5, 6, 7)) 63 | let b = Edge.init(wrappedValue: (1, 2, 3, 4, 5, 6, 7)) 64 | 65 | XCTAssertNotEqual(a, b) 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/VergeTests/FilterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterTests.swift 3 | // VergeCoreTests 4 | // 5 | // Created by muukii on 2020/01/14. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | 13 | import Verge 14 | 15 | final class FilterTests: XCTestCase { 16 | 17 | func testCombinedFilterOR() { 18 | 19 | struct Model { 20 | var a = 0 21 | var b = 0 22 | var c = 0 23 | } 24 | 25 | let comparison = OrComparator( 26 | .any { @Sendable in $0.a == 1 && $1.a == 1 }, 27 | .any { @Sendable in $0.b == 1 && $1.b == 1 } 28 | ) 29 | .or(.any { @Sendable in $0.c == 1 && $1.c == 1 }) 30 | 31 | do { 32 | let pre = Model() 33 | let new = Model() 34 | XCTAssertEqual(comparison(pre, new), false) 35 | } 36 | 37 | do { 38 | var pre = Model() 39 | pre.a = 1 40 | var new = Model() 41 | new.a = 1 42 | XCTAssertEqual(comparison(pre, new), true) 43 | } 44 | 45 | do { 46 | var pre = Model() 47 | pre.c = 1 48 | var new = Model() 49 | new.c = 1 50 | XCTAssertEqual(comparison(pre, new), true) 51 | } 52 | 53 | } 54 | 55 | func testCombinedFilterAnd() { 56 | 57 | struct Model { 58 | var a = 0 59 | var b = 0 60 | var c = 0 61 | } 62 | 63 | let comparison = AndComparator( 64 | .any { @Sendable in $0.a == 1 && $1.a == 1 }, 65 | .any { @Sendable in $0.b == 1 && $1.b == 1 } 66 | ) 67 | .and(.any { @Sendable in $0.c == 1 && $1.c == 1 }) 68 | 69 | do { 70 | let pre = Model() 71 | let new = Model() 72 | XCTAssertEqual(comparison(pre, new), false) 73 | } 74 | 75 | do { 76 | var pre = Model() 77 | pre.a = 1 78 | var new = Model() 79 | new.a = 1 80 | XCTAssertEqual(comparison(pre, new), false) 81 | } 82 | 83 | do { 84 | var pre = Model() 85 | pre.a = 1 86 | pre.b = 1 87 | var new = Model() 88 | new.a = 1 89 | new.b = 1 90 | XCTAssertEqual(comparison(pre, new), false) 91 | } 92 | 93 | do { 94 | var pre = Model() 95 | pre.a = 1 96 | pre.b = 1 97 | pre.c = 1 98 | var new = Model() 99 | new.a = 1 100 | new.b = 1 101 | new.c = 1 102 | XCTAssertEqual(comparison(pre, new), true) 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/VergeTests/IsolatedContextTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | 4 | @MainActor 5 | fileprivate func isInMain() {} 6 | 7 | final class IsolatedContextTests: XCTestCase { 8 | 9 | func testMainActorSubscription() { 10 | 11 | Task { @MainActor in 12 | isInMain() 13 | } 14 | 15 | let store = DemoStore() 16 | 17 | _ = store.sinkState { changes in 18 | isInMain() 19 | } 20 | 21 | _ = store.sinkState(queue: .main) { @MainActor changes in 22 | isInMain() 23 | } 24 | 25 | _ = store.sinkState(queue: .mainIsolated()) { @MainActor changes in 26 | isInMain() 27 | } 28 | 29 | _ = store.sinkState(queue: .asyncSerialBackground) { changes in 30 | Task { @MainActor in 31 | isInMain() 32 | } 33 | } 34 | 35 | } 36 | 37 | func testMainActorSubscription_sink() { 38 | 39 | // don't add `@MainActor` to make non-isolated-context 40 | 41 | assert(Thread.isMainThread) 42 | 43 | let store = DemoStore() 44 | 45 | var receivedState: DemoStore.State? 46 | 47 | let sub = store.sinkState { changes in 48 | receivedState = changes.primitive 49 | } 50 | 51 | store.commit { 52 | $0.count = 100 53 | } 54 | 55 | XCTAssertEqual(receivedState?.count, 100) 56 | 57 | withExtendedLifetime(sub, {}) 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Tests/VergeTests/OldComparer.swift: -------------------------------------------------------------------------------- 1 | /// A component that compares an input value. 2 | /// It can be combined with other comparers. 3 | public struct Comparer { 4 | 5 | public static var alwaysFalse: Self { 6 | .init { _, _ in false } 7 | } 8 | 9 | private let _equals: (Input, Input) -> Bool 10 | 11 | /// Creates an instance 12 | /// 13 | /// - Parameter equals: Return true if two inputs are equal. 14 | public init( 15 | _ equals: @escaping (Input, Input) -> Bool 16 | ) { 17 | self._equals = equals 18 | } 19 | 20 | /// It compares the value selected from passed selector closure 21 | /// - Parameter selector: 22 | public init(selector: @escaping (Input) -> T) { 23 | self.init { a, b in 24 | selector(a) == selector(b) 25 | } 26 | } 27 | 28 | public init(selector: @escaping (Input) -> T, equals: @escaping (T, T) -> Bool) { 29 | self.init { a, b in 30 | equals(selector(a), selector(b)) 31 | } 32 | } 33 | 34 | public init(selector: @escaping (Input) -> T, comparer: Comparer) { 35 | self.init { a, b in 36 | comparer._equals(selector(a), selector(b)) 37 | } 38 | } 39 | 40 | /// Make Combined comparer 41 | /// - Parameter comparers: 42 | public init(and comparers: [Comparer]) { 43 | self.init { pre, new in 44 | for filter in comparers { 45 | guard filter._equals(pre, new) else { 46 | return false 47 | } 48 | } 49 | return true 50 | } 51 | } 52 | 53 | /// Make Combined comparer 54 | /// - Parameter comparers: 55 | public init(or comparers: [Comparer]) { 56 | self.init { pre, new in 57 | for filter in comparers { 58 | if filter._equals(pre, new) { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | } 65 | 66 | public func equals(_ lhs: Input, _ rhs: Input) -> Bool { 67 | _equals(lhs, rhs) 68 | } 69 | 70 | /// Returns an curried closure 71 | public func curried() -> (_ lhs: Input, _ rhs: Input) -> Bool { 72 | _equals 73 | } 74 | 75 | } 76 | 77 | extension Comparer where Input : Equatable { 78 | public init() { 79 | self.init(==) 80 | } 81 | 82 | public static var usingEquatable: Self { 83 | return .init(==) 84 | } 85 | } 86 | 87 | extension Comparer { 88 | 89 | public func and(_ otherComparer: () -> Comparer) -> Comparer { 90 | .init(and: [ 91 | self, 92 | otherComparer() 93 | ]) 94 | } 95 | 96 | public func or(_ otherComparer: () -> Comparer) -> Comparer { 97 | .init(or: [ 98 | self, 99 | otherComparer() 100 | ]) 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Tests/VergeTests/PerformanceTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | import XCTest 5 | import Verge 6 | 7 | /** 8 | Store's state contains a huge dictionary. 9 | This test-case tests to commit mutations: 10 | 1. Mutates property beside of huge dictionary. 11 | 2. Mutates a huge dictionary. 12 | It compares that performance. 13 | It would be good if the first 1 test-case is fast without unaffected from a huge dictionary. 14 | */ 15 | class PerformanceTests: XCTestCase { 16 | 17 | func testMutationOnAnotherProperty() { 18 | 19 | let store = DemoStore() 20 | 21 | measure(metrics: [XCTMemoryMetric(), XCTCPUMetric(), XCTClockMetric()]) { 22 | store.increment() 23 | } 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Tests/VergeTests/PipelineTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Verge 3 | 4 | final class PipelineTests: XCTestCase { 5 | 6 | func test_MapPipeline() { 7 | 8 | let mapCounter = VergeConcurrency.UnfairLockAtomic.init(0) 9 | 10 | let pipeline = Pipelines.ChangesMapPipeline( 11 | intermediate: { 12 | $0.count 13 | }, 14 | transform: { 15 | mapCounter.modify { 16 | $0 += 1 17 | } 18 | return $0 19 | }, 20 | additionalDropCondition: nil 21 | ) 22 | 23 | var storage: Void = () 24 | 25 | do { 26 | let s = DemoState() 27 | 28 | XCTAssertEqual( 29 | pipeline.yieldContinuously( 30 | Changes.init( 31 | old: s, 32 | new: s 33 | ), 34 | storage: &storage 35 | ), 36 | .noUpdates 37 | ) 38 | 39 | XCTAssertEqual(mapCounter.value, 0) 40 | } 41 | 42 | do { 43 | 44 | XCTAssertEqual( 45 | pipeline.yieldContinuously( 46 | Changes.init( 47 | old: .init(name: "A", count: 1), 48 | new: .init(name: "A", count: 2) 49 | ), 50 | storage: &storage 51 | ), 52 | .new(2) 53 | ) 54 | 55 | XCTAssertEqual(mapCounter.value , 2) 56 | 57 | } 58 | 59 | } 60 | 61 | func test_MapPipeline_Intermediate() { 62 | 63 | let mapCounter = VergeConcurrency.UnfairLockAtomic.init(0) 64 | 65 | let pipeline = Pipelines.ChangesMapPipeline( 66 | intermediate: { 67 | $0.name 68 | }, 69 | transform: { 70 | mapCounter.modify { $0 += 1 } 71 | return $0.count 72 | }, 73 | additionalDropCondition: nil 74 | ) 75 | 76 | var storage: Void = () 77 | 78 | do { 79 | let s = DemoState() 80 | 81 | XCTAssertEqual( 82 | pipeline.yieldContinuously( 83 | Changes.init( 84 | old: s, 85 | new: s 86 | ), 87 | storage: &storage 88 | ), 89 | .noUpdates 90 | ) 91 | 92 | XCTAssertEqual(mapCounter.value, 0) 93 | } 94 | 95 | do { 96 | 97 | XCTAssertEqual( 98 | pipeline.yieldContinuously( 99 | Changes.init( 100 | old: .init(name: "A", count: 1), 101 | new: .init(name: "A", count: 2) 102 | ), 103 | storage: &storage 104 | ), 105 | .noUpdates 106 | ) 107 | 108 | XCTAssertEqual(mapCounter.value, 0) 109 | 110 | } 111 | 112 | } 113 | 114 | func testSelect() { 115 | 116 | do { 117 | let store = Verge.Store(initialState: .init()) 118 | // do { 119 | // let d = store.derived(.map(\.nonEquatable)) 120 | // XCTAssert((d as Any) is Derived>) 121 | // } 122 | // 123 | // do { 124 | // let d = store.derived(.map(\.nonEquatable)) 125 | // XCTAssert((d as Any) is Derived>) 126 | // } 127 | 128 | do { 129 | let d = store.derived(.map(\.onEquatable)) 130 | XCTAssert((d as Any) is Derived) 131 | } 132 | 133 | do { 134 | let d = store.derived(.map(\.count)) 135 | XCTAssert((d as Any) is Derived) 136 | } 137 | 138 | do { 139 | let d = store.derived(.map { @Sendable in $0.count }) 140 | XCTAssert((d as Any) is Derived) 141 | } 142 | } 143 | 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /Tests/VergeTests/ReferenceEdgeTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import Verge 4 | 5 | final class FragmentTests: XCTestCase { 6 | 7 | struct State { 8 | var name: String = "" 9 | @ReferenceEdge var number = 0 10 | } 11 | 12 | func testFragment() { 13 | 14 | var state = State() 15 | XCTAssertEqual(state.number, 0) 16 | 17 | state.number += 1 18 | 19 | XCTAssertEqual(state.number, 1) 20 | } 21 | 22 | func testFragmentWithCopy() { 23 | 24 | let state = State() 25 | XCTAssertEqual(state.number, 0) 26 | 27 | var anotherState = state 28 | anotherState.number += 1 29 | 30 | XCTAssertEqual(anotherState.number, 1) 31 | 32 | XCTAssertNotEqual(state.$number._storagePointer, anotherState.$number._storagePointer) 33 | } 34 | 35 | func testReference() { 36 | 37 | var state = State() 38 | 39 | state.number += 1 40 | 41 | state.name = "A" 42 | 43 | var state2 = state 44 | state2.name = "B" 45 | 46 | XCTAssert(state.$number._storagePointer == state2.$number._storagePointer) 47 | 48 | } 49 | 50 | func testCast() { 51 | 52 | let source = ReferenceEdge(wrappedValue: State()) 53 | 54 | var binded = unsafeBitCast(source, to: ReferenceEdge.self) 55 | 56 | print(binded.name) 57 | 58 | binded.name = "hiroshi" 59 | 60 | XCTAssertEqual(binded.name, "hiroshi") 61 | 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Tests/VergeTests/RunLoopTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Verge 3 | 4 | final class RunLoopTests: XCTestCase { 5 | 6 | func test_performance_adding() { 7 | 8 | measure { 9 | for _ in 0..<1000 { 10 | RunLoopActivityObserver.addObserver(acitivity: .beforeWaiting, in: .main) { 11 | } 12 | } 13 | } 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Tests/VergeTests/Sample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sample.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2020/04/18. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if canImport(UIKIt) 12 | import UIKit 13 | 14 | import Verge 15 | 16 | enum Sample { 17 | 18 | struct State: StateType { 19 | var name: String = "" 20 | var age: Int = 0 21 | } 22 | 23 | enum Activity { 24 | case somethingHappen 25 | } 26 | 27 | class ViewController: UIViewController { 28 | 29 | private let nameLabel: UILabel = .init() 30 | private let ageLabel: UILabel = .init() 31 | 32 | let store = Store(initialState: .init(), logger: nil) 33 | 34 | var subscriptions = Set() 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | store.sinkState { [weak self] (changes) in 40 | self?.update(changes: changes) 41 | } 42 | .store(in: &subscriptions) 43 | 44 | } 45 | 46 | private func update(changes: Changes) { 47 | 48 | changes.ifChanged(\.name) { (name) in 49 | nameLabel.text = name 50 | } 51 | 52 | changes.ifChanged(\.age) { (age) in 53 | ageLabel.text = age.description 54 | } 55 | 56 | } 57 | 58 | } 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Tests/VergeTests/StateTypeTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Verge 3 | import XCTest 4 | 5 | final class StateTypeTests: XCTestCase { 6 | 7 | func test_init() { 8 | 9 | let store = Store(initialState: .init()) 10 | 11 | XCTAssertEqual(store.state.count2, 1) 12 | 13 | } 14 | 15 | func test_store() { 16 | 17 | let store = Store(initialState: .init()) 18 | 19 | store.commit { 20 | $0.count = 1 21 | } 22 | 23 | XCTAssertEqual(store.state.count2, 2) 24 | 25 | store.commit { 26 | $0.name = "a" 27 | } 28 | 29 | XCTAssertEqual(store.state.count2, 2) 30 | } 31 | 32 | struct State: StateType { 33 | 34 | var name = "" 35 | var count = 0 36 | var count2 = 0 37 | 38 | static func reduce( 39 | modifying: inout StateTypeTests.State, 40 | transaction: inout Transaction, 41 | current: Changes 42 | ) { 43 | 44 | current.ifChanged(\.count).do { _ in 45 | modifying.count2 += 1 46 | } 47 | } 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Tests/VergeTests/StoreAndDerivedTests.swift: -------------------------------------------------------------------------------- 1 | import Verge 2 | import XCTest 3 | 4 | final class StoreAndDerivedTests: XCTestCase { 5 | 6 | @MainActor 7 | func test() async { 8 | 9 | let store = Store<_, Never>(initialState: DemoState()) 10 | 11 | let _ = store.derived(.select(\.name)) 12 | let countDerived = store.derived(.select(\.count)) 13 | 14 | await withTaskGroup(of: Void.self) { group in 15 | 16 | for i in 0..<1000 { 17 | group.addTask { 18 | await withBackground { 19 | store.commit { 20 | $0.name = "\(i)" 21 | } 22 | } 23 | 24 | } 25 | } 26 | 27 | group.addTask { 28 | await withBackground { 29 | store.commit { 30 | $0.count = 100 31 | } 32 | } 33 | 34 | 35 | XCTAssertEqual(store.state.count, 100) 36 | 37 | await store.waitUntilAllEventConsumed() 38 | // potentially it fails as EventEmitter's behavior 39 | // If EventEmitter's buffer is not empty, commit function escape from the stack by only adding. 40 | XCTAssertEqual(countDerived.state.primitive, 100) 41 | } 42 | 43 | } 44 | 45 | print("end") 46 | 47 | } 48 | 49 | } 50 | 51 | /** 52 | Performs the given task in background 53 | */ 54 | public nonisolated func withBackground( 55 | _ thunk: @escaping @Sendable () async throws -> Return 56 | ) async rethrows -> Return { 57 | 58 | // for now we will keep this until Swift6. 59 | assert(Thread.isMainThread == false) 60 | 61 | // here is the background as it's nonisolated 62 | // to inherit current actor context, use @_unsafeInheritExecutor 63 | 64 | // thunk closure runs on the background as it's sendable 65 | // if it's not sendable, inherit current actor context but it's already background. 66 | // @_inheritActorContext makes closure runs on current actor context even if it's sendable. 67 | return try await thunk() 68 | } 69 | -------------------------------------------------------------------------------- /Tests/VergeTests/StoreInitTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import Verge 4 | 5 | final class StoreInitTests: XCTestCase { 6 | 7 | class RefState { 8 | 9 | } 10 | /* 11 | class RefViewModel: StoreComponentType { 12 | 13 | class State: Equatable { 14 | static func == (lhs: StoreInitTests.RefViewModel.State, rhs: StoreInitTests.RefViewModel.State) -> Bool { 15 | true 16 | } 17 | 18 | init() {} 19 | } 20 | 21 | let store: DefaultStore 22 | 23 | init() { 24 | 25 | // it raises a warning 26 | self.store = DefaultStore(initialState: .init()) 27 | } 28 | } 29 | */ 30 | 31 | class StructViewModel: StoreDriverType { 32 | 33 | struct State: Equatable { 34 | 35 | init() {} 36 | } 37 | 38 | let store: Store 39 | 40 | init() { 41 | 42 | self.store = .init(initialState: .init()) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Tests/VergeTests/StoreMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | 3 | @Suite("StoreMiddlewareTests") 4 | struct StoreMiddlewareTests { 5 | 6 | @Test("Commit Hook") 7 | func testCommitHook() { 8 | let store = DemoStore() 9 | 10 | store.add( 11 | middleware: .modify { @Sendable modifyingState, transaction, current in 12 | current.ifChanged(\.count).do { _ in 13 | modifyingState.count += 1 14 | } 15 | }) 16 | 17 | store.add( 18 | middleware: .modify { @Sendable modifyingState, transaction, current in 19 | current.ifChanged(\.name).do { _ in 20 | modifyingState.count = 100 21 | } 22 | }) 23 | 24 | #expect(store.state.count == 0) 25 | 26 | store.commit { 27 | $0.count += 1 28 | } 29 | 30 | #expect(store.state.count == 2) 31 | 32 | store.commit { 33 | $0.name = "A" 34 | } 35 | 36 | if case .graph(let graph) = store.state.modification { 37 | graph.prettyPrint() 38 | #expect( 39 | graph.prettyPrint() == """ 40 | VergeTests.DemoState { 41 | name-(1)+(1) 42 | count-(1)+(1) 43 | } 44 | """) 45 | } 46 | 47 | #expect(store.state.count == 100) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/VergeTests/StoreSinkTests.swift: -------------------------------------------------------------------------------- 1 | import Verge 2 | import XCTest 3 | 4 | @MainActor 5 | fileprivate func UI() { 6 | 7 | } 8 | 9 | final class StoreSinkTests: XCTestCase { 10 | 11 | func testMainActorSubscription() { 12 | 13 | let store = DemoStore() 14 | 15 | _ = store.sinkState { _ in 16 | UI() 17 | } 18 | 19 | _ = store.sinkState(queue: .main) { @MainActor _ in 20 | UI() 21 | } 22 | 23 | } 24 | 25 | func testNonActorSubscription() { 26 | 27 | let store = DemoStore() 28 | 29 | _ = store.sinkState(queue: .passthrough) { _ in 30 | Task { 31 | await UI() 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/VergeTests/StoreTaskTests.swift: -------------------------------------------------------------------------------- 1 | import Verge 2 | import XCTest 3 | import Atomics 4 | 5 | final class StoreTaskTests: XCTestCase { 6 | 7 | func test() { 8 | 9 | let atomic = ManagedAtomic.init(false) 10 | 11 | do { 12 | let r = atomic.compareExchange(expected: true, desired: true, ordering: .sequentiallyConsistent).exchanged 13 | print(r) 14 | } 15 | 16 | do { 17 | let r = atomic.compareExchange(expected: true, desired: false, ordering: .sequentiallyConsistent).exchanged 18 | print(r) 19 | } 20 | 21 | do { 22 | let r = atomic.compareExchange(expected: false, desired: true, ordering: .sequentiallyConsistent).exchanged 23 | print(r) 24 | } 25 | 26 | } 27 | 28 | @MainActor 29 | func testActorContext_onMainActor() async throws { 30 | 31 | let store = DemoStore() 32 | 33 | try await store.task { 34 | XCTAssertTrue(Thread.isMainThread) 35 | } 36 | .value 37 | 38 | try await store.taskDetached { 39 | XCTAssertFalse(Thread.isMainThread) 40 | } 41 | .value 42 | } 43 | 44 | func testActorContext() async throws { 45 | 46 | let store = DemoStore() 47 | 48 | try await store.task { 49 | XCTAssertFalse(Thread.isMainThread) 50 | } 51 | .value 52 | 53 | try await store.taskDetached { 54 | XCTAssertFalse(Thread.isMainThread) 55 | } 56 | .value 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/VergeTests/SynchronizeDisplayValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynchronizeDisplayValueTests.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2019/11/09. 6 | // Copyright © 2019 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | 13 | import Verge 14 | -------------------------------------------------------------------------------- /Tests/VergeTests/SyntaxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxTests.swift 3 | // VergeStoreTests 4 | // 5 | // Created by muukii on 2020/05/24. 6 | // Copyright © 2020 muukii. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import Verge 12 | import VergeMacros 13 | 14 | enum SyntaxTests { 15 | 16 | static func code() { 17 | 18 | let changes: Changes = .init(old: nil, new: .init()) 19 | 20 | changes.ifChanged(\.name).do { name in 21 | 22 | } 23 | 24 | // changes.ifChanged({ ($0.name, $0.name) }) { args in 25 | // } 26 | 27 | changes.ifChanged({ $0.nonEquatable }, comparator: .alwaysFalse()).do { name in 28 | 29 | } 30 | 31 | changes.ifChanged({ $0.name }).do { name in 32 | 33 | } 34 | 35 | changes.ifChanged(\.name, \.count).do { name, count in 36 | 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/VergeTests/TransactionTests.swift: -------------------------------------------------------------------------------- 1 | import Verge 2 | import XCTest 3 | 4 | final class TransactionTests: XCTestCase { 5 | 6 | func testTransaction() async { 7 | 8 | struct MyKey: TransactionKey { 9 | static var defaultValue: String? { 10 | nil 11 | } 12 | } 13 | 14 | let store = Store(initialState: .init()) 15 | 16 | await store.commit { 17 | $0.count += 1 18 | $1[MyKey.self] = "first commit" 19 | } 20 | 21 | XCTAssertEqual(store.state._transaction[MyKey.self], "first commit") 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Tests/VergeTests/Usage.swift: -------------------------------------------------------------------------------- 1 | 2 | import Verge 3 | 4 | struct RootState: Equatable { 5 | 6 | struct Nested: Equatable {} 7 | 8 | var nested: Nested = .init() 9 | 10 | } 11 | 12 | final class RootStore: Store, @unchecked Sendable { 13 | 14 | } 15 | 16 | final class Service: StoreDriverType { 17 | 18 | var store: RootStore { fatalError() } 19 | 20 | init() { 21 | 22 | } 23 | 24 | } 25 | 26 | final class SpecificService: StoreDriverType { 27 | 28 | var scope: WritableKeyPath & Sendable { \.nested } 29 | 30 | var store: RootStore { fatalError() } 31 | 32 | init() { 33 | 34 | _ = sinkState { state in 35 | 36 | let _: Changes = state 37 | } 38 | 39 | } 40 | 41 | } 42 | 43 | final class ViewModel: StoreDriverType { 44 | 45 | struct State: Equatable {} 46 | 47 | let store: Store = .init(initialState: .init()) 48 | 49 | init() { 50 | 51 | commit { _ in 52 | 53 | } 54 | 55 | _ = sinkState { state in 56 | let _: Changes = state 57 | } 58 | 59 | } 60 | 61 | } 62 | 63 | // MARK: - Abstraction 64 | 65 | protocol MyViewModelType: StoreDriverType where TargetStore.State == String, TargetStore.Activity == Int { 66 | 67 | } 68 | 69 | final class Concrete: MyViewModelType { 70 | 71 | let store: Verge.Store 72 | 73 | init() { 74 | store = .init(initialState: .init()) 75 | 76 | _ = sinkState { _ in 77 | 78 | } 79 | 80 | commit { _ in 81 | 82 | } 83 | 84 | } 85 | 86 | } 87 | 88 | final class Controller where ViewModel.Scope == ViewModel.TargetStore.State { 89 | 90 | init(viewModel: ViewModel) { 91 | 92 | let _: Changes = viewModel.state 93 | 94 | viewModel.send(1) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Verge.playground/Pages/Memo.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | 2 | enum Action { 3 | case increment 4 | case decrement 5 | } 6 | 7 | class Store { 8 | 9 | var state: State 10 | } 11 | -------------------------------------------------------------------------------- /Verge.playground/Pages/PlainStorePattern1.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewA: DemoUIView { 4 | 5 | // MARK: Components 6 | 7 | let nameLabel = UILabel() 8 | 9 | // MARK: State 10 | 11 | var name: String = "" { 12 | didSet { 13 | self.nameLabel.text = name 14 | } 15 | } 16 | 17 | } 18 | 19 | //: [Next](@next) 20 | 21 | -------------------------------------------------------------------------------- /Verge.playground/Pages/PlainStorePattern2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import UIKit 4 | 5 | class ViewA: DemoUIView { 6 | 7 | // MARK: Components 8 | 9 | let nameLabel = UILabel() 10 | let ageLabel = UILabel() 11 | 12 | // MARK: State 13 | 14 | var name: String = "" { 15 | didSet { 16 | self.nameLabel.text = name 17 | } 18 | } 19 | 20 | var age: Int = 0 { 21 | didSet { 22 | self.ageLabel.text = name 23 | } 24 | } 25 | 26 | } 27 | 28 | //: [Next](@next) 29 | -------------------------------------------------------------------------------- /Verge.playground/Pages/PlainStorePattern3.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | import UIKit 3 | 4 | class ViewA: DemoUIView { 5 | 6 | // MARK: Components 7 | 8 | let nameLabel = UILabel() 9 | let ageLabel = UILabel() 10 | 11 | // MARK: State 12 | 13 | struct State { 14 | var name: String = "" 15 | var age: Int = 0 16 | } 17 | 18 | var state: State { 19 | didSet { 20 | self.nameLabel.text = state.name 21 | self.ageLabel.text = state.age.description 22 | } 23 | } 24 | 25 | } 26 | //: [Next](@next) 27 | -------------------------------------------------------------------------------- /Verge.playground/Pages/PlainStorePattern4.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | 5 | import UIKit 6 | 7 | struct State { 8 | var name: String = "" 9 | var age: Int = 0 10 | } 11 | 12 | class Store { 13 | 14 | // State type should be struct type to call didSet 15 | var state: State { 16 | didSet { 17 | // notify a update to subscribers 18 | } 19 | } 20 | 21 | init(_ state: State) { 22 | self.state = state 23 | } 24 | 25 | func onDidUpdate(_ closure: @escaping (State) -> Void) { 26 | // implement to notfy updates 27 | } 28 | } 29 | 30 | class ViewA: DemoUIView { 31 | 32 | // MARK: Components 33 | 34 | let nameLabel = UILabel() 35 | let ageLabel = UILabel() 36 | 37 | // MARK: State 38 | 39 | let store: Store 40 | 41 | // MARK: - Initializers 42 | 43 | init(store: Store) { 44 | self.store = store 45 | 46 | super.init() 47 | 48 | store.onDidUpdate { [weak self] state in 49 | 50 | guard let self = self else { return } 51 | 52 | self.ageLabel.text = state.age.description 53 | self.nameLabel.text = state.name 54 | } 55 | } 56 | 57 | private func updateName() { 58 | store.state.name = "Muukii" 59 | // then, nameLabel updates to "Muukii" by store.onDidUpdate. 60 | } 61 | 62 | } 63 | 64 | class ViewB: DemoUIView { 65 | 66 | // MARK: Components 67 | 68 | let nameLabel = UILabel() 69 | let ageLabel = UILabel() 70 | 71 | // MARK: State 72 | 73 | let store: Store 74 | 75 | // MARK: - Initializers 76 | 77 | init(store: Store) { 78 | self.store = store 79 | 80 | super.init() 81 | 82 | store.onDidUpdate { [weak self] state in 83 | 84 | guard let self = self else { return } 85 | 86 | self.ageLabel.text = state.age.description 87 | self.nameLabel.text = state.name 88 | } 89 | } 90 | 91 | } 92 | //: [Next](@next) 93 | -------------------------------------------------------------------------------- /Verge.playground/Pages/PlainStorePattern5.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | 6 | struct State { 7 | var name: String = "" 8 | var age: Int = 0 9 | } 10 | 11 | class Store { 12 | 13 | // State type should be struct type to call didSet 14 | var state: State { 15 | didSet { 16 | // notify a update to subscribers 17 | let old = oldValue 18 | let new = state 19 | } 20 | } 21 | 22 | init(_ state: State) { 23 | self.state = state 24 | } 25 | 26 | func onDidUpdate(_ closure: @escaping (_ old: State, _ new: State) -> Void) { 27 | // implement to notfy updates 28 | } 29 | } 30 | 31 | class ViewA: DemoUIView { 32 | 33 | // MARK: Components 34 | 35 | let nameLabel = UILabel() 36 | let ageLabel = UILabel() 37 | 38 | // MARK: State 39 | 40 | let store: Store 41 | 42 | // MARK: - Initializers 43 | 44 | init(store: Store) { 45 | self.store = store 46 | 47 | super.init() 48 | 49 | store.onDidUpdate { [weak self] old, new in 50 | 51 | guard let self = self else { return } 52 | 53 | if old.name != new.name { 54 | self.nameLabel.text = new.name 55 | } 56 | if old.age != new.age { 57 | self.ageLabel.text = new.age.description 58 | } 59 | } 60 | } 61 | 62 | private func updateName() { 63 | store.state.name = "Muukii" 64 | // then, nameLabel updates to "Muukii" by store.onDidUpdate. 65 | } 66 | 67 | } 68 | 69 | class ViewB: DemoUIView { 70 | 71 | // MARK: Components 72 | 73 | let nameLabel = UILabel() 74 | let ageLabel = UILabel() 75 | 76 | // MARK: State 77 | 78 | let store: Store 79 | 80 | // MARK: - Initializers 81 | 82 | init(store: Store) { 83 | self.store = store 84 | 85 | super.init() 86 | 87 | store.onDidUpdate { [weak self] old, new in 88 | 89 | guard let self = self else { return } 90 | 91 | if old.name != new.name { 92 | self.nameLabel.text = new.name 93 | } 94 | if old.age != new.age { 95 | self.ageLabel.text = new.age.description 96 | } 97 | } 98 | } 99 | 100 | } 101 | 102 | //: [Next](@next) 103 | -------------------------------------------------------------------------------- /Verge.playground/Pages/VergeStorePartern2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | import UIKit 3 | 4 | import Verge 5 | 6 | struct State { 7 | var name: String = "" 8 | var age: Int = 0 9 | } 10 | 11 | class Store: VergeStore.Store { 12 | 13 | init() { 14 | super.init(initialState: State(), logger: nil) 15 | } 16 | 17 | func incrementAge() { 18 | commit { 19 | $0.age += 1 20 | } 21 | } 22 | 23 | func updateName(_ name: String) { 24 | commit { 25 | $0.name = name 26 | } 27 | } 28 | } 29 | 30 | class ViewA: DemoUIView { 31 | 32 | let nameLabel = UILabel() 33 | let ageLabel = UILabel() 34 | 35 | let store: Store 36 | private var cancellable: VergeAnyCancellable? 37 | 38 | init(store: Store) { 39 | self.store = store 40 | 41 | super.init() 42 | 43 | cancellable = store.sinkChanges { [weak self] (changes) in 44 | self?.updateUI(changes: changes) 45 | } 46 | 47 | } 48 | 49 | private func updateUI(changes: Changes) { 50 | 51 | changes.ifChanged(\.name) { name in 52 | nameLabel.text = name 53 | } 54 | 55 | changes.ifChanged(\.age) { age in 56 | ageLabel.text = age.description 57 | } 58 | } 59 | 60 | } 61 | 62 | //: [Next](@next) 63 | -------------------------------------------------------------------------------- /Verge.playground/Pages/VergeStorePattern.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | 5 | import UIKit 6 | import Verge 7 | 8 | struct State { 9 | var name: String = "" 10 | var age: Int = 0 11 | } 12 | 13 | class Store: VergeStore.Store { 14 | 15 | init() { 16 | super.init(initialState: State(), logger: nil) 17 | } 18 | } 19 | 20 | class ViewA: DemoUIView { 21 | 22 | // MARK: Components 23 | 24 | let nameLabel = UILabel() 25 | let ageLabel = UILabel() 26 | 27 | // MARK: State 28 | 29 | let store: Store 30 | private var cancellable: VergeAnyCancellable? 31 | 32 | // MARK: - Initializers 33 | 34 | init(store: Store) { 35 | self.store = store 36 | 37 | super.init() 38 | 39 | cancellable = store.sinkChanges { [weak self] (changes) in 40 | 41 | guard let self = self else { return } 42 | 43 | changes.ifChanged(\.name) { name in 44 | self.nameLabel.text = name 45 | } 46 | 47 | changes.ifChanged(\.age) { age in 48 | self.ageLabel.text = age.description 49 | } 50 | 51 | } 52 | 53 | } 54 | 55 | private func updateName() { 56 | store.commit { 57 | $0.name = "Muukii" 58 | } 59 | } 60 | 61 | } 62 | //: [Next](@next) 63 | -------------------------------------------------------------------------------- /Verge.playground/Sources/Wire.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class DemoUIView: UIView { 4 | 5 | public init() { 6 | super.init(frame: .zero) 7 | } 8 | 9 | @available(*, unavailable) 10 | public required init?(coder: NSCoder) { 11 | fatalError("init(coder:) has not been implemented") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Verge.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | tuist = "4.44.3" 3 | -------------------------------------------------------------------------------- /playgrounds/.gitignore: -------------------------------------------------------------------------------- 1 | !*.xcodeproj -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "91b2bd4d42ac7a19338699d5fcae987277fc6461718b80e90459a6235bafabb2", 3 | "pins" : [ 4 | { 5 | "identity" : "normalization", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/VergeGroup/Normalization", 8 | "state" : { 9 | "revision" : "6e7cb1ddeda4d0f1d2fbf8ca6d25ecd8ed6ba917", 10 | "version" : "1.1.0" 11 | } 12 | }, 13 | { 14 | "identity" : "rxswift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/ReactiveX/RxSwift.git", 17 | "state" : { 18 | "revision" : "c7c7d2cf50a3211fe2843f76869c698e4e417930", 19 | "version" : "6.8.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-atomics", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-atomics.git", 26 | "state" : { 27 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 28 | "version" : "1.2.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-collections", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-collections", 35 | "state" : { 36 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 37 | "version" : "1.1.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-concurrency-task-manager", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/VergeGroup/swift-concurrency-task-manager", 44 | "state" : { 45 | "revision" : "340cf14e0282977deeeb436605d1810ce4f4fbc9", 46 | "version" : "1.4.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-macro-state-struct", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/VergeGroup/swift-macro-state-struct", 53 | "state" : { 54 | "branch" : "main", 55 | "revision" : "88e3937abd26b76bafe9027938538966457f156f" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-syntax", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-syntax.git", 62 | "state" : { 63 | "revision" : "0687f71944021d616d34d922343dcef086855920", 64 | "version" : "600.0.1" 65 | } 66 | }, 67 | { 68 | "identity" : "typedcomparator", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/VergeGroup/TypedComparator", 71 | "state" : { 72 | "revision" : "337ce0e573e7637ddd29392cb88c3b852f042c8e", 73 | "version" : "1.0.0" 74 | } 75 | }, 76 | { 77 | "identity" : "typedidentifier", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/VergeGroup/TypedIdentifier", 80 | "state" : { 81 | "revision" : "284340409ba47858a1b3c353dcef9c21303953db", 82 | "version" : "2.0.3" 83 | } 84 | } 85 | ], 86 | "version" : 3 87 | } 88 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/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 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/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 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // PlaySwiftUI 4 | // 5 | // Created by Muukii on 2025/02/13. 6 | // 7 | 8 | import SwiftUI 9 | import Verge 10 | 11 | @Tracking 12 | struct ContentState { 13 | 14 | @Tracking 15 | struct Info { 16 | 17 | let constant: String 18 | var name: String 19 | 20 | init(name: String = "Init") { 21 | self.constant = name 22 | self.name = name 23 | } 24 | } 25 | 26 | var info = Info() 27 | 28 | var count: Int = 0 29 | } 30 | 31 | enum AppContainer { 32 | static let store = Store<_, Never>(initialState: ContentState()) 33 | } 34 | 35 | struct ContentView: View { 36 | 37 | let store = AppContainer.store 38 | 39 | var body: some View { 40 | VStack { 41 | 42 | Button("Update Name") { 43 | store.commit { 44 | $0.info.name = UUID().uuidString 45 | } 46 | } 47 | 48 | Button("Increment") { 49 | store.commit { 50 | $0.count += 1 51 | } 52 | } 53 | 54 | Button("Replace info") { 55 | store.commit { 56 | $0.info = .init(name: "Replaced") 57 | } 58 | } 59 | 60 | StoreReader(store) { (state: ContentState) in 61 | Text(state.info.name) 62 | Text(state.count.description) 63 | } 64 | 65 | StoreReader(store) { (state: ContentState) in 66 | Text(state.info.constant) 67 | } 68 | 69 | @StoreBindable var storeBindable = store 70 | TextField("Name", text: $storeBindable.info.name) 71 | 72 | } 73 | .padding() 74 | } 75 | } 76 | 77 | #Preview { 78 | ContentView() 79 | } 80 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/PlaySwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaySwiftUIApp.swift 3 | // PlaySwiftUI 4 | // 5 | // Created by Muukii on 2025/02/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct PlaySwiftUIApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /playgrounds/PlaySwiftUI/PlaySwiftUI/Simple.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Verge 3 | 4 | private struct _Book: View { 5 | 6 | @Tracking 7 | struct State { 8 | var count: Int = 0 9 | 10 | @Tracking 11 | struct NestedState { 12 | var isActive: Bool = false 13 | var message: String = "Hello, Verge!" 14 | } 15 | 16 | var nestedState: NestedState = NestedState() 17 | } 18 | 19 | let store = Store<_, Never>(initialState: State()) 20 | 21 | var body: some View { 22 | StoreReader(store) { state in 23 | VStack { 24 | Text("Count: \(state.count)") 25 | Button("Increment") { 26 | store.commit { 27 | $0.count += 1 28 | } 29 | } 30 | Text("Is Active: \(state.nestedState.isActive)") 31 | Text("Message: \(state.nestedState.message)") 32 | Button("Toggle Active") { 33 | store.commit { 34 | $0.nestedState.isActive.toggle() 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | #Preview("Simple") { 43 | _Book() 44 | } 45 | --------------------------------------------------------------------------------