├── Sources
└── CoreEngine
│ ├── Assets
│ └── .gitkeep
│ ├── Classes
│ ├── .gitkeep
│ ├── Core.swift
│ ├── PublisherCore.swift
│ ├── AsyncCore.swift
│ └── Async
│ │ └── AsyncCoreSequence.swift
│ └── Misc
│ └── DidPublished.swift
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── CoreEngine.xcscheme
├── Dockerfile
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── Package.swift
├── Tests
└── CoreEngineTests
│ ├── DidPublishedTest.swift
│ └── AsyncCoreTest.swift
├── CoreEngine.podspec
└── README.md
/Sources/CoreEngine/Assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sources/CoreEngine/Classes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 |
2 | FROM swift:5.8 as build
3 |
4 | WORKDIR /app
5 |
6 | COPY Package.swift .
7 | COPY .swiftpm .
8 | COPY Sources ./Sources
9 | COPY Tests ./Tests
10 |
11 | RUN swift package resolve
12 | RUN swift build -c release
13 | # RUN swift test --parallel
14 | FROM swift:5.8-slim
15 |
16 | WORKDIR /app
17 |
18 | COPY --from=build /app/.build/release /app/build
19 |
20 | CMD ["./CoreEngine"]
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Integration Core Engine
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | pull_request:
9 | branches:
10 | - '*'
11 |
12 | jobs:
13 | deploy:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Set up SSH
21 | uses: webfactory/ssh-agent@v0.5.3
22 | with:
23 | ssh-private-key: ${{ secrets.VULTR_SSH_PRIVATE_KEY }}
24 |
25 | - name: Build Docker image
26 | run: docker build -t swift-app .
27 |
--------------------------------------------------------------------------------
/Sources/CoreEngine/Misc/DidPublished.swift:
--------------------------------------------------------------------------------
1 | #if canImport(Combine)
2 | import Combine
3 |
4 | @propertyWrapper
5 | public struct DidPublished {
6 | private var value: Value
7 | private let subject = PassthroughSubject()
8 |
9 | public var wrappedValue: Value {
10 | get { value }
11 | set {
12 | value = newValue
13 | subject.send(newValue)
14 | }
15 | }
16 |
17 | public var projectedValue: AnyPublisher {
18 | subject.eraseToAnyPublisher()
19 | }
20 |
21 | public init(wrappedValue: Value) {
22 | self.value = wrappedValue
23 | }
24 | }
25 | #endif
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | *.moved-aside
17 | DerivedData
18 | *.hmap
19 | *.ipa
20 | .build/
21 | # Bundler
22 | .bundle
23 |
24 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
25 | # Carthage/Checkouts
26 |
27 | Carthage/Build
28 |
29 | # We recommend against adding the Pods directory to your .gitignore. However
30 | # you should judge for yourself, the pros and cons are mentioned at:
31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
32 | #
33 | # Note: if you ignore the Pods directory, make sure to uncomment
34 | # `pod install` in .travis.yml
35 | #
36 | # Pods/
37 |
--------------------------------------------------------------------------------
/Sources/CoreEngine/Classes/Core.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @dynamicMemberLookup
4 | @dynamicCallable
5 | public protocol Core: AnyObject {
6 | associatedtype Action
7 | associatedtype State
8 |
9 | var action: ((Action) -> ()) { get }
10 | var state: State { get set }
11 | func reduce(state: State, action: Action) -> State
12 | }
13 |
14 | extension Core {
15 | public var action: ((Action) -> ()) {
16 | let _newActionClosure: ((Action) -> ()) = { [weak self] _action in
17 | if let self = self {
18 | self.state = self.reduce(state: self.state, action: _action)
19 | }
20 | }
21 | return _newActionClosure
22 | }
23 |
24 | public func dynamicallyCall(withArguments actions: [Action]) {
25 | actions.forEach({ self.action($0) })
26 | }
27 |
28 | public subscript(dynamicMember keyPath: KeyPath) -> T {
29 | return state[keyPath: keyPath]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 stareta1202
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "CoreEngine",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "CoreEngine",
16 | targets: ["CoreEngine"]),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | // .package(url: /* package url */, from: "1.0.0"),
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
25 | .target(
26 | name: "CoreEngine",
27 | dependencies: []),
28 | .testTarget(
29 | name: "CoreEngineTests",
30 | dependencies: [
31 | "CoreEngine"
32 | ],
33 | path: "Tests/CoreEngineTests"
34 | )
35 |
36 | ]
37 | )
38 |
--------------------------------------------------------------------------------
/Sources/CoreEngine/Classes/PublisherCore.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if canImport(Combine)
3 | import Combine
4 |
5 | public protocol PublisherCore: Core, ObservableObject {
6 | typealias Error = Swift.Error
7 |
8 | var subscription: Set { get set }
9 | func dispatch(effect: any Publisher)
10 | func dispatch(effect: any Publisher)
11 |
12 | func handleError(error: Error)
13 | }
14 |
15 | extension PublisherCore {
16 | public func dispatch(effect: any Publisher) {
17 | effect
18 | .sink { [weak self] completion in
19 | if case let .failure(error) = completion {
20 | self?.handleError(error: error)
21 | }
22 | } receiveValue: { [weak self] value in
23 | if let self {
24 | self.state = self.reduce(state: self.state, action: value)
25 | }
26 | }
27 | .store(in: &self.subscription)
28 | }
29 |
30 | public func dispatch(effect: any Publisher) {
31 | effect
32 | .sink(receiveValue: { [weak self] value in
33 | if let self {
34 | self.state = self.reduce(state: self.state, action: value)
35 | }
36 | })
37 | .store(in: &self.subscription)
38 | }
39 |
40 | public func handleError(error: Error) { }
41 | }
42 |
43 | @available(*, deprecated, renamed: "PublisherCore", message: "Use PublisherCore instead")
44 | public typealias AnyCore = PublisherCore
45 | #endif
46 |
--------------------------------------------------------------------------------
/Sources/CoreEngine/Classes/AsyncCore.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol AsyncCore: Actor {
4 | associatedtype Action
5 | associatedtype State
6 |
7 | var action: ((Action) async -> ()) { get }
8 | nonisolated var currentState: State { get set }
9 | var states: AsyncCoreSequence { get }
10 | var continuation: AsyncStream.Continuation { get }
11 |
12 | func reduce(state: State, action: Action) async throws -> State
13 | func handleError(error: Error) async
14 | }
15 |
16 | public extension AsyncCore {
17 | var action: ((Action) async -> ()) {
18 | let newActionClosure: ((Action) async -> ()) = { [weak self] _action in
19 | guard let self = self else { return }
20 | do {
21 | let reducedState = try await self.reduce(state: self.currentState, action: _action)
22 | await self.update(state: reducedState)
23 | } catch {
24 | await self.handleError(error: error)
25 | }
26 | }
27 | return newActionClosure
28 | }
29 |
30 | func dynamicallyCall(withArguments actions: [Action]) async {
31 | for action in actions {
32 | await self.action(action)
33 | }
34 | }
35 |
36 | subscript(dynamicMember keyPath: KeyPath) -> T {
37 | return currentState[keyPath: keyPath]
38 | }
39 |
40 | nonisolated func send(_ action: Action) {
41 | Task {
42 | await self.action(action)
43 | }
44 | }
45 |
46 | func handleError(error: Error) async { }
47 |
48 | func select(_ selector: @escaping @Sendable (State) -> Selected) -> AsyncMapSequence, Selected> {
49 | states.map(selector)
50 | }
51 | }
52 |
53 | private extension AsyncCore {
54 | private func update(state: State) async {
55 | self.currentState = state
56 | continuation.yield(self.currentState)
57 | self.states.send(state)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/CoreEngineTests/DidPublishedTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | #if canImport(Combine)
3 | import Combine
4 |
5 | //@testable import YourModuleName // Replace with the name of your module
6 | @testable import CoreEngine
7 |
8 | class DidPublishedTests: XCTestCase {
9 | var cancellables: Set = []
10 |
11 | override func tearDown() {
12 | cancellables.removeAll()
13 | super.tearDown()
14 | }
15 |
16 | // Test to verify that the wrapped value changes correctly
17 | func testWrappedValueAssignment() {
18 | class TestObject {
19 | @DidPublished var value: Int = 0
20 | }
21 |
22 | let testObject = TestObject()
23 | XCTAssertEqual(testObject.value, 0)
24 |
25 | testObject.value = 10
26 | XCTAssertEqual(testObject.value, 10)
27 | }
28 |
29 | // Test to verify that the publisher emits values when the wrapped value changes
30 | func testPublisherEmitsValues() {
31 | class TestObject {
32 | @DidPublished var value: String = "Initial"
33 | }
34 |
35 | let testObject = TestObject()
36 | var receivedValues: [String] = []
37 |
38 | testObject.$value
39 | .sink { value in
40 | receivedValues.append(value)
41 | }
42 | .store(in: &cancellables)
43 |
44 | testObject.value = "First Update"
45 | testObject.value = "Second Update"
46 |
47 | // Allow some time for the publisher to emit values
48 | XCTAssertEqual(receivedValues, ["First Update", "Second Update"])
49 | }
50 |
51 | // Test to verify that ObservableObject integration works
52 | func testObservableObjectIntegration() async {
53 | class TestObject: ObservableObject {
54 | @DidPublished var value: Double = 0.0
55 | }
56 |
57 | let objectWillChangedExpectation = XCTestExpectation(description: "DidCalled object will changed")
58 | let valuePublisherExpectation = XCTestExpectation(description: "value was changed")
59 |
60 | let testObject = TestObject()
61 |
62 |
63 | testObject.$value
64 | .sink { _ in
65 | valuePublisherExpectation.fulfill()
66 | }
67 | .store(in: &cancellables)
68 |
69 | testObject.value = 3.14
70 |
71 | await fulfillment(of: [ valuePublisherExpectation], timeout: 5)
72 |
73 |
74 | XCTAssertEqual(testObject.value, 3.14)
75 | }
76 | }
77 | #endif
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/CoreEngine.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
35 |
41 |
42 |
43 |
44 |
45 |
55 |
56 |
62 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/CoreEngine.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint CoreEngine.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'CoreEngine'
11 | s.version = '1.1.0'
12 | s.summary = '🌪️ Simple and light-weighted unidirectional Data Flow in Swift'
13 |
14 | # This description is used to generate tags and improve search results.
15 | # * Think: What does it do? Why did you write it? What is the focus?
16 | # * Try to keep it short, snappy and to the point.
17 | # * Write the description between the DESC delimiters below.
18 | # * Finally, don't worry about the indent, CocoaPods strips it!
19 |
20 | s.description = <<-DESC
21 |
22 | # CoreKit
23 |
24 | Core is a framework for making more reactive applications inspired by [ReactorKit](https://github.com/ReactorKit/ReactorKit), [Redux](http://redux.js.org/docs/basics/index.html) with Combine. It's a very light weigthed and simple architecture, so you can either use CocoaPods or SPM to stay up to date, or just drag and drop into your project and go. Or you can look through it and roll your own.
25 |
26 | ## Example
27 |
28 | See details on Example
29 |
30 | ```swift
31 | // on ViewController
32 | let label = UILabel()
33 | let increaseButton = UIButton()
34 | let decreaseButton = UIButton()
35 | var core: MainCore = .init()
36 |
37 | func increaseButtonTapped() {
38 | self.core.action(.increase)
39 | }
40 |
41 | func decreaseButtonTapped() {
42 | self.core.action(.decrease)
43 | }
44 |
45 | func bind() {
46 | core.$state.map(\.count)
47 | .sink { [weak self] count in
48 | self?.label.text = "\(count)"
49 | }
50 | .store(in: &subscription)
51 | }
52 | ...
53 | ```
54 |
55 | ```swift
56 | class MainCore: Core {
57 | var action: Action? = nil
58 |
59 | var subscription: Set = .init()
60 |
61 | enum Action: Equatable, Hashable {
62 | case increase
63 | case decrease
64 | }
65 |
66 | struct State: Equatable {
67 | var count = 0
68 | }
69 |
70 | @Published var state: State = .init()
71 |
72 | func reduce(state: State, action: Action) -> State {
73 | var newState = state
74 | switch action {
75 | case .decrease:
76 | newState.count -= 1
77 | case .increase:
78 | newState.count += 1
79 | }
80 | return newState
81 | }
82 | }
83 |
84 | ```
85 | DESC
86 |
87 | s.homepage = 'https://github.com/stareta1202/CoreEngine'
88 | s.license = { :type => 'MIT', :file => 'LICENSE' }
89 | s.author = { 'stareta1202' => 'stareta1202@gmail.com' }
90 | s.source = { :git => 'https://github.com/stareta1202/CoreEngine.git', :tag => s.version.to_s }
91 | s.social_media_url = 'https://www.sobabear.com'
92 |
93 | s.ios.deployment_target = '13.0'
94 |
95 | s.source_files = 'Sources/CoreEngine/Classes/**/*'
96 |
97 | # s.resource_bundles = {
98 | # 'CoreEngine' => ['CoreEngine/Assets/*.png']
99 | # }
100 |
101 | # s.public_header_files = 'Pod/Classes/**/*.h'
102 | # s.frameworks = 'UIKit', 'MapKit'
103 | # s.dependency 'AFNetworking', '~> 2.3'
104 | end
105 |
--------------------------------------------------------------------------------
/Tests/CoreEngineTests/AsyncCoreTest.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import CoreEngine
3 | import XCTest
4 |
5 | actor MyAsyncCore: @preconcurrency AsyncCore {
6 |
7 | var states: AsyncCoreSequence
8 | var continuation: AsyncStream.Continuation
9 |
10 | init(initialState: State) {
11 | self.currentState = initialState
12 | let (stream, continuation) = AsyncStream.makeStream()
13 |
14 | self.states = .init(stream)
15 | self.continuation = continuation
16 | }
17 |
18 |
19 | enum Action {
20 | case increment
21 | case decrement
22 | case sleepAndIncreaseTen
23 | }
24 |
25 | struct State: Equatable {
26 | var count: Int
27 | }
28 |
29 | nonisolated(unsafe) var currentState: State
30 |
31 | func reduce(state: State, action: Action) async throws -> State {
32 | var newState = state
33 | switch action {
34 | case .increment:
35 | newState.count += 1
36 | case .decrement:
37 | newState.count -= 1
38 | case .sleepAndIncreaseTen:
39 | try! await Task.sleep(nanoseconds: 1_000_000_000)
40 | newState.count += 10
41 | }
42 |
43 | return newState
44 | }
45 |
46 | func handleError(error: Error) async {
47 | print("Error: \(error.localizedDescription)")
48 | }
49 | }
50 |
51 | final class AsyncCoreTests: XCTestCase {
52 |
53 |
54 |
55 | // Test multiple actions in sequence
56 | func testMultipleActions() async {
57 | let core = MyAsyncCore(initialState: .init(count: 0))
58 |
59 | await core.action(.increment)
60 | await core.action(.increment)
61 | await core.action(.increment)
62 |
63 | var actionCount = 0
64 |
65 | for await count in await core.states.map(\.count) {
66 | actionCount += 1
67 | print("wow count is\(count)")
68 | if actionCount == 3 {
69 | break
70 | }
71 | }
72 | }
73 |
74 | func testMultipleSemd() {
75 | let core = MyAsyncCore(initialState: .init(count: 0))
76 |
77 | core.send(.increment)
78 | core.send(.increment)
79 | core.send(.increment)
80 |
81 | Task {
82 | var actionCount = 0
83 | for await count in await core.states.map(\.count) {
84 | actionCount += 1
85 | print("wow count is\(count)")
86 | if actionCount == 3 {
87 | break
88 | }
89 | }
90 | }
91 |
92 | }
93 |
94 | func testCurrentValues() async {
95 | let core = MyAsyncCore(initialState: .init(count: 0))
96 |
97 |
98 | await core.action(.increment)
99 | let count1 = core.currentState.count
100 | XCTAssertEqual(count1, 1)
101 |
102 |
103 |
104 | await core.action(.increment)
105 | let count2 = core.currentState.count
106 | XCTAssertEqual(count2, 2)
107 |
108 |
109 | await core.action(.increment)
110 | let count3 = core.currentState.count
111 | XCTAssertEqual(count3, 3)
112 |
113 | await core.action(.decrement)
114 | let count4 = core.currentState.count
115 | XCTAssertEqual(count4, 2)
116 | }
117 |
118 | func testEstimateTime() async {
119 | let core = MyAsyncCore(initialState: .init(count: 0))
120 | let startTime = Date().timeIntervalSince1970
121 |
122 | await core.action(.sleepAndIncreaseTen)
123 | await core.action(.sleepAndIncreaseTen)
124 | await core.action(.sleepAndIncreaseTen)
125 | await core.action(.decrement)
126 |
127 | await core.action(.sleepAndIncreaseTen)
128 |
129 |
130 | let endTime = Date().timeIntervalSince1970
131 |
132 | XCTAssertLessThanOrEqual(endTime - startTime, 5)
133 | XCTAssertEqual(core.currentState.count, 39)
134 |
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Sources/CoreEngine/Classes/Async/AsyncCoreSequence.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @dynamicMemberLookup
4 | public final class AsyncCoreSequence: AsyncSequence {
5 | public typealias Element = State
6 | public struct Iterator: AsyncIteratorProtocol {
7 | public typealias Element = State
8 |
9 | @usableFromInline
10 | var iterator: AsyncStream.Iterator
11 |
12 | @usableFromInline
13 | init(_ iterator: AsyncStream.Iterator) {
14 | self.iterator = iterator
15 | }
16 |
17 | @inlinable
18 | public mutating func next() async -> Element? {
19 | await iterator.next()
20 | }
21 | }
22 |
23 | private let stream: AsyncStream
24 | private var continuations: [AsyncStream.Continuation] = []
25 | private var last: State?
26 |
27 | public init(_ stream: AsyncStream) {
28 | self.stream = stream
29 | }
30 |
31 | deinit {
32 | continuations.forEach { $0.finish() }
33 | }
34 |
35 | public nonisolated func makeAsyncIterator() -> Iterator {
36 | Iterator(stream.makeAsyncIterator())
37 | }
38 |
39 | public func send(_ state: State) {
40 | last = state
41 | for continuation in continuations {
42 | continuation.yield(state)
43 | }
44 | }
45 |
46 | public subscript(
47 | dynamicMember keyPath: KeyPath
48 | ) -> AsyncMapSequence, Property> {
49 | let (stream, continuation) = AsyncStream.createStream()
50 | continuations.append(continuation)
51 | if let last {
52 | continuation.yield(last)
53 | }
54 | return stream.map { $0[keyPath: keyPath] }
55 | }
56 | }
57 |
58 | public extension AsyncCoreSequence {
59 | /// Custom `map` function to map over state properties using key paths.
60 | func map(
61 | _ keyPath: KeyPath
62 | ) -> AsyncMapSequence, Property> {
63 | let (stream, continuation) = AsyncStream.createStream()
64 | continuations.append(continuation)
65 |
66 | if let lastState = last {
67 | continuation.yield(lastState)
68 | }
69 |
70 | return stream.map { $0[keyPath: keyPath] }
71 | }
72 |
73 | func map(
74 | _ transform: @Sendable @escaping (State) async -> Transformed
75 | ) -> AsyncMapSequence, Transformed> {
76 | let (stream, continuation) = AsyncStream.createStream()
77 | continuations.append(continuation)
78 | if let last = last {
79 | continuation.yield(last)
80 | }
81 | return stream.map(transform)
82 | }
83 |
84 | func map(
85 | _ transform: @escaping (State) async throws -> Transformed
86 | ) -> AsyncThrowingMapSequence, Transformed> {
87 | let (stream, continuation) = AsyncStream.createStream()
88 | continuations.append(continuation)
89 | if let last = last {
90 | continuation.yield(last)
91 | }
92 | return stream.map(transform)
93 | }
94 |
95 | func filter(
96 | _ isIncluded: @escaping (State) async -> Bool
97 | ) -> AsyncFilterSequence> {
98 | let (stream, continuation) = AsyncStream.createStream()
99 | continuations.append(continuation)
100 | if let last = last {
101 | continuation.yield(last)
102 | }
103 | return stream.filter(isIncluded)
104 | }
105 |
106 | func dropFirst(_ count: Int = 1) -> AsyncDropFirstSequence> {
107 | let (stream, continuation) = AsyncStream.createStream()
108 | continuations.append(continuation)
109 | if let last = last {
110 | continuation.yield(last)
111 | }
112 | return stream.dropFirst(count)
113 | }
114 |
115 | // func flatMap(
116 | // _ transform: @escaping (State) async -> SegmentOfResult
117 | // ) -> AsyncFlatMapSequence, SegmentOfResult> {
118 | // let (stream, continuation) = AsyncStream.createStream()
119 | // continuations.append(continuation)
120 | // if let last = last {
121 | // continuation.yield(last)
122 | // }
123 | // return stream.flatMap(transform)
124 | // }
125 |
126 | func flatMap(
127 | _ transform: @escaping (State) async throws -> SegmentOfResult
128 | ) -> AsyncThrowingFlatMapSequence, SegmentOfResult> {
129 | let (stream, continuation) = AsyncStream.createStream()
130 | continuations.append(continuation)
131 | if let last = last {
132 | continuation.yield(last)
133 | }
134 | return stream.flatMap(transform)
135 | }
136 |
137 | func drop(
138 | while predicate: @escaping (State) async -> Bool
139 | ) -> AsyncDropWhileSequence> {
140 | let (stream, continuation) = AsyncStream.createStream()
141 | continuations.append(continuation)
142 | if let last = last {
143 | continuation.yield(last)
144 | }
145 | return stream.drop(while: predicate)
146 | }
147 | }
148 |
149 |
150 | extension AsyncSequence {
151 | /// makeStream is not supported on Linux
152 | static func createStream(
153 | of elementType: Element.Type = Element.self,
154 | bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded
155 | ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) {
156 | var continuation: AsyncStream.Continuation!
157 | let stream = AsyncStream(bufferingPolicy: limit) { cont in
158 | continuation = cont
159 | }
160 | return (stream, continuation)
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CoreEngine
2 | [](https://github.com/sobabear/CoreEngine/actions/workflows/ci.yml)
3 | [](https://github.com/sobabear/CoreEngine/releases)
4 | [](https://cocoapods.org/pods/CoreEngine)
5 |
6 | ### Simple and light
7 | 
8 | Core is a framework for making more reactive applications inspired by [ReactorKit](https://github.com/ReactorKit/ReactorKit), [Redux](http://redux.js.org/docs/basics/index.html).
9 | ### Scalability
10 | Core is Reactive independent Framework which means you can expand whatever you want to import such as [Combine](https://developer.apple.com/documentation/combine), [RxSwift](https://github.com/ReactiveX/RxSwift).
11 |
12 | It's a very light weigthed and simple architecture, so you can either use CocoaPods or SPM to stay up to date, or just drag and drop into your project and go. Or you can look through it and roll your own.
13 |
14 | ## Installation
15 | CoreEngine is available through [CocoaPods](https://cocoapods.org). To install
16 | it, simply add the following line to your Podfile:
17 |
18 | ### CocoaPods
19 |
20 | ```ruby
21 | pod 'CoreEngine'
22 | ```
23 |
24 | ### Swift Package Manager
25 |
26 | [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
27 |
28 |
29 | To integrate SnapKit into your Xcode project using Swift Package Manager, add it to the dependencies value of your `Package.swift`:
30 |
31 | ```swift
32 | dependencies: [
33 | .package(url: "https://github.com/sobabear/CoreEngine.git", .upToNextMajor(from: "1.3.1"))
34 | ]
35 | ```
36 |
37 | ## Performance
38 |
39 |
40 |
41 | Core Engine is insanely fast and light-weight compared to similar frameworks
42 | you can check details here [CoreEngineBenchMark](https://github.com/sobabear/CoreEngineBenchMark)
43 |
44 |
45 | ## Highly Recommended: Using AsyncCore
46 |
47 | While CoreEngine provides both traditional reactive approaches and state management patterns, we highly recommend using AsyncCore for modern, asynchronous, and more efficient state handling.
48 |
49 | AsyncCore leverages Swift's structured concurrency with async/await, providing a clean and intuitive way to manage state updates, handle side effects, and ensure thread safety with Swift's Actor model.
50 |
51 | ### Why Use AsyncCore?
52 | - Concurrency-First: Built for Swift's native concurrency model, using async/await to handle asynchronous actions.
53 | - Simplified State Management: AsyncCore simplifies reactive architecture by providing built-in async state streams and an efficient way to dispatch actions.
54 | - Error Handling and Side Effects: AsyncCore includes streamlined error handling and support for asynchronous side effects.
55 | - Thread Safety: Since AsyncCore is built on top of Swift’s Actor system, it ensures thread-safe access to state and actions.
56 |
57 | ## Example
58 |
59 | See details on Example
60 |
61 | ```swift
62 | /// on ViewController with Core
63 | let label = UILabel()
64 | let increaseButton = UIButton()
65 | let decreaseButton = UIButton()
66 | var core: MainCore = .init()
67 |
68 | func increaseButtonTapped() {
69 | self.core.action(.increase)
70 | }
71 |
72 | func decreaseButtonTapped() {
73 | self.core.action(.decrease)
74 | }
75 |
76 | func multipleActions() {
77 | self.core.action(.increase, .decrease)
78 | }
79 |
80 |
81 | func bind() {
82 | core.$state.map(\.count)
83 | .sink { [weak self] count in
84 | self?.label.text = "\(count)"
85 | }
86 | .store(in: &subscription)
87 | }
88 | ...
89 |
90 | /// on ViewController with AsyncCore
91 | class ViewController: UIViewController {
92 | private var core: AsyncMainCore?
93 |
94 | override func viewDidLoad() {
95 | super.viewDidLoad()
96 |
97 | Task {
98 | let core = await AsyncMainCore(initialState: .init())
99 | self.core = core
100 | self.bind(core: core)
101 | }
102 | }
103 |
104 | private func bind(core: AsyncMainCore) {
105 | Task {
106 | for await count in core.states.compactMap(\.count) {
107 | print("Count: \(count)")
108 | }
109 | }
110 | Task {
111 | for await count in core.states.count {
112 | print("Count: \(count)")
113 | }
114 | }
115 | }
116 |
117 | private func bind() {
118 | Task {
119 | if let counts = self.core?.states.count {
120 | for await count in counts {
121 | print("Count: \(count)")
122 | }
123 | }
124 | }
125 | }
126 |
127 |
128 | @IBAction func increaseTapped() {
129 | core?.send(.increase)
130 | }
131 |
132 | @IBAction func decreaseTapped() {
133 | core?.send(.decrease)
134 | }
135 | }
136 |
137 | ```
138 |
139 | ```swift
140 | actor AsyncMainCore: AsyncCore {
141 | var currentState: State
142 |
143 | enum Action: Equatable, Hashable {
144 | case increase
145 | case decrease
146 | }
147 |
148 | struct State: Equatable, Sendable {
149 | var count = 0
150 | }
151 | nonisolated(unsafe) var currentState: State = .init()
152 | var states: AsyncCoreSequence
153 | var continuation: AsyncStream.Continuation
154 |
155 | init(initialState: State) async {
156 | self.currentState = initialState
157 | let (states, continuation) = AsyncStream.makeStream()
158 | self.states = await .init(states)
159 | self.continuation = continuation
160 | }
161 |
162 | func reduce(state: State, action: Action) async -> State {
163 | var newState = state
164 | switch action {
165 | case .increase:
166 | newState.count += 1
167 | case .decrease:
168 | newState.count -= 1
169 | }
170 | return newState
171 | }
172 | }
173 |
174 | class MainCore: Core {
175 | var subscription: Set = .init()
176 |
177 | enum Action: Equatable, Hashable {
178 | case increase
179 | case decrease
180 | }
181 |
182 | struct State: Equatable {
183 | var count = 0
184 | }
185 |
186 | @Published var state: State = .init()
187 |
188 | func reduce(state: State, action: Action) -> State {
189 | var newState = state
190 | switch action {
191 | case .decrease:
192 | newState.count -= 1
193 | case .increase:
194 | newState.count += 1
195 | }
196 | return newState
197 | }
198 | }
199 |
200 | ```
201 |
202 |
203 | ## Side Effect & Error Handling
204 |
205 | Not just simple core, but complex core is also supported. For example, Side Effect and Error handling. When it comes to those, you use ```AsyncCore or PublisherCore```.
206 |
207 | It is not very different from Core, since AnyCore also conforms.
208 |
209 | ### func dispatch(effect: any Publisher) & func handleError(error: Error)
210 | This method is defined in AnyCore and when you deal with side-effect generated publisher send into the function. Also you can handle every errors on the ```handleError(error: Error)``` function
211 |
212 | Here is an example of the ``` PublisherCore```
213 |
214 |
215 | ```swift
216 | class MainCore: PublisherCore {
217 | var subscription: Set = .init()
218 |
219 | enum Action {
220 | case increase
221 | case decrease
222 | case jump(Int)
223 | case setNumber(Int)
224 | }
225 |
226 | struct State {
227 | var count = 0
228 | }
229 |
230 | @Published var state: State = .init()
231 | @Published var tenGap: Int = 10
232 |
233 | private let sessionService = SessionService()
234 |
235 | init {
236 | dispatch(effect: sessionService.randomUInt$().map(Action.setNumber))
237 | }
238 |
239 | func reduce(state: State, action: Action) -> State {
240 | var newState = state
241 |
242 | switch action {
243 | case .increase:
244 | newState.count += 1
245 | case .decrease:
246 | newState.count -= 1
247 | case let .jump(value):
248 | newState.count += value
249 | case let .setNumber(value):
250 | newState.count = value
251 | }
252 | return newState
253 | }
254 |
255 | func handleError(error: Error) {
256 | if let errpr = error as? MyError {
257 | //handle
258 | }
259 | }
260 |
261 | func tenJumpAction() {
262 | self.dispatch(effect: $tenGap.map(Action.jump))
263 | }
264 | }
265 |
266 |
267 | class SessionService {
268 | func randomUInt$() -> AnyPublisher {
269 | // blahblah
270 | }
271 | }
272 |
273 |
274 | ```
275 | ## Examples + RxSwift
276 |
277 | copy those code for RxSwift
278 | ```swift
279 | import Foundation
280 | import CoreEngine
281 | import RxSwift
282 |
283 | protocol RxCore: Core {
284 | var disposeBag: DisposeBag { get set }
285 |
286 | func mutate(effect: Observable)
287 | func handleError(error: Error)
288 | }
289 |
290 | extension RxCore {
291 | public func mutate(effect: Observable) {
292 | effect
293 | .subscribe(onNext: { [weak self] in
294 | if let self {
295 | self.state = self.reduce(state: self.state, action: $0)
296 | }
297 |
298 | }, onError: { [weak self] in
299 | self?.handleError(error: $0)
300 | })
301 | .disposed(by: disposeBag)
302 | }
303 |
304 | public func handleError(error: Error) { }
305 | }
306 |
307 |
308 | @propertyWrapper
309 | class ObservableProperty: ObservableType {
310 | var wrappedValue: Element {
311 | didSet {
312 | subject.onNext(wrappedValue)
313 | }
314 | }
315 |
316 | private let subject: BehaviorSubject
317 |
318 | init(wrappedValue: Element) {
319 | self.wrappedValue = wrappedValue
320 | self.subject = BehaviorSubject(value: wrappedValue)
321 | }
322 |
323 | var projectedValue: Observable {
324 | return subject.asObservable()
325 | }
326 |
327 | func subscribe(_ observer: Observer) -> Disposable where Observer : ObserverType, Element == Observer.Element {
328 | return subject.subscribe(observer)
329 | }
330 | }
331 |
332 | ```
333 |
334 | ## Author
335 |
336 | stareta1202, stareta1202@gmail.com
337 |
338 | ## License
339 |
340 | CoreEngine is available under the MIT license. See the LICENSE file for more info.
341 |
342 |
343 |
344 |
--------------------------------------------------------------------------------