Skip to content
This repository was archived by the owner on Oct 14, 2024. It is now read-only.

Commit 27d767b

Browse files
committed
Merge branch 'develop'
2 parents ed636f4 + 41ad9e6 commit 27d767b

File tree

11 files changed

+199
-198
lines changed

11 files changed

+199
-198
lines changed

Demo/IOS/UI/CounterScreen.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ struct CounterScreen: ConnectedNodeDescription, PlasticNodeDescription, PlasticR
5050
$0.setKey(Keys.decrementButton)
5151
$0.titles[.normal] = "Decrement"
5252
$0.backgroundColor = .dogwoodRose
53-
$0.titleColors = [.highlighted : .jet]
53+
$0.titleColors = [.highlighted: .jet]
5454

5555
$0.touchHandlers = [
56-
.touchUpInside : {
56+
.touchUpInside: {
5757
dispatch(DecrementCounter())
5858
}
5959
]
@@ -62,10 +62,10 @@ struct CounterScreen: ConnectedNodeDescription, PlasticNodeDescription, PlasticR
6262
$0.setKey(Keys.incrementButton)
6363
$0.titles[.normal] = "Increment"
6464
$0.backgroundColor = .japaneseIndigo
65-
$0.titleColors = [.highlighted : .jet]
65+
$0.titleColors = [.highlighted: .jet]
6666

6767
$0.touchHandlers = [
68-
.touchUpInside : {
68+
.touchUpInside: {
6969
dispatch(IncrementCounter())
7070
}
7171
]

Examples/CodingLove/CodingLove/Actions/FetchMorePosts.swift

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import Foundation
1010
import Katana
1111

1212
struct FetchMorePosts: AsyncAction, ActionWithSideEffect {
13-
1413
public struct CompletedActionPayload {
1514
var posts: [Post]
1615
var allFetched: Bool = false
@@ -56,26 +55,26 @@ struct FetchMorePosts: AsyncAction, ActionWithSideEffect {
5655
}
5756

5857
public func sideEffect(
59-
state: State,
60-
dispatch: @escaping StoreDispatch,
61-
dependencies: SideEffectDependencyContainer
62-
) {
63-
64-
let castedState = state as! CodingLoveState
65-
let page: Int = castedState.page
66-
67-
let postsProvider = dependencies as! PostsProvider
68-
69-
postsProvider.fetchPosts(for: page) { (result, errorMessage) in
70-
if let data = result {
71-
let (posts, allFetched) = data
72-
dispatch(self.completedAction {
73-
$0.completedPayload = CompletedActionPayload(posts: posts, allFetched: allFetched)
74-
})
75-
76-
} else {
77-
dispatch(self.failedAction { $0.failedPayload = errorMessage! })
78-
}
58+
currentState: State,
59+
previousState: State,
60+
dispatch: @escaping StoreDispatch,
61+
dependencies: SideEffectDependencyContainer) {
62+
63+
let castedState = currentState as! CodingLoveState
64+
let page: Int = castedState.page
65+
66+
let postsProvider = dependencies as! PostsProvider
67+
68+
postsProvider.fetchPosts(for: page) { (result, errorMessage) in
69+
if let data = result {
70+
let (posts, allFetched) = data
71+
dispatch(self.completedAction {
72+
$0.completedPayload = CompletedActionPayload(posts: posts, allFetched: allFetched)
73+
})
74+
75+
} else {
76+
dispatch(self.failedAction { $0.failedPayload = errorMessage! })
7977
}
78+
}
8079
}
8180
}

Examples/CodingLove/CodingLove/Providers/PostsProvider.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,8 @@ import Katana
1111

1212
let postsPerPage = 3
1313

14-
struct PostsProvider: SideEffectDependencyContainer {
15-
var posts: [Post]
16-
17-
public init(state: State, dispatch: @escaping StoreDispatch) {
18-
self.posts = [Post]()
19-
}
14+
class PostsProvider: SideEffectDependencyContainer {
15+
public required init(dispatch: @escaping StoreDispatch) {}
2016

2117
public func fetchPosts(for page: Int, completion: @escaping (([Post], Bool)?, String?) -> ()) {
2218
DispatchQueue.global().async {

Katana/Core/Animations/AnimationUtils.swift

Lines changed: 82 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ import Foundation
1111
/// Namespace for some animation utilities methods
1212
struct AnimationUtils {
1313
private init () {}
14-
14+
1515
/// The intermediate steps of the 4 step animation
1616
enum AnimationStep {
1717
/// The first intermediate step
1818
case firstIntermediate
19-
19+
2020
/// The second intermediate step
2121
case secondIntermediate
2222
}
23-
23+
2424
/**
2525
Merges `descriptions` with `other` using a strategy that depends on the step.
2626

@@ -42,72 +42,21 @@ struct AnimationUtils {
4242
- parameter step: the step of the process
4343
- returns: an array of descriptions with elements merged following the algorithm described above
4444

45-
*/
45+
*/
4646
static func mergedDescriptions(
4747
_ descriptions: [AnyNodeDescription],
4848
_ other: [AnyNodeDescription],
4949
step: AnimationStep) -> [AnyNodeDescription] {
50-
51-
let firstArray: [AnyNodeDescription]
52-
let secondArray: [AnyNodeDescription]
53-
50+
5451
switch step {
5552
case .firstIntermediate:
56-
firstArray = descriptions
57-
secondArray = other
58-
53+
return self.mergedDescriptions(descriptions, with: other)
54+
5955
case .secondIntermediate:
60-
firstArray = other
61-
secondArray = descriptions
62-
}
63-
64-
// lookup for first array
65-
var firstArrayLookup = [Int: Int]()
66-
for (index, item) in firstArray.enumerated() {
67-
firstArrayLookup[item.replaceKey] = index
68-
}
69-
70-
// lookup for second array
71-
var secondArrayLookup = [Int: Int]()
72-
for (index, item) in secondArray.enumerated() {
73-
secondArrayLookup[item.replaceKey] = index
74-
}
75-
76-
var result = firstArray
77-
var added = 0
78-
var firstArrayMaxPosition = -1
79-
80-
for item in secondArray {
81-
let position = firstArrayLookup[item.replaceKey]
82-
83-
if let position = position {
84-
// we already have the item
85-
firstArrayMaxPosition = max(firstArrayMaxPosition, position)
86-
continue
87-
}
88-
89-
result.insert(item, at: added + firstArrayMaxPosition + 1)
90-
added = added + 1
56+
return self.mergedDescriptions(other, with: descriptions)
9157
}
92-
93-
// merge also children, if needed
94-
result = result.map { description in
95-
guard var propsWithChildren = description.anyProps as? Childrenable else {
96-
return description
97-
}
98-
99-
let secondItemChildren = secondArrayLookup[description.replaceKey]
100-
.flatMap({ secondArray[$0] })
101-
.flatMap({ $0 as? AnyNodeDescriptionWithChildren })
102-
.flatMap({ $0.children })
103-
104-
propsWithChildren.children = mergedDescriptions(propsWithChildren.children, secondItemChildren ?? [], step: step)
105-
return type(of: description).init(anyProps: propsWithChildren as! AnyNodeDescriptionProps)
106-
}
107-
108-
return result
10958
}
110-
59+
11160
/**
11261
Updates the descriptions using an instance of `ChildrenAnimations`.
11362

@@ -124,63 +73,124 @@ struct AnimationUtils {
12473
- parameter targetChildren: the target children used to define when apply a transformation
12574
- parameter step: the step of the process
12675
- returns: an array of updated descriptions
127-
*/
76+
*/
12877
static func updatedDescriptions(
12978
for descriptions: [AnyNodeDescription],
13079
using childrenAnimation: AnyChildrenAnimations,
13180
targetChildren: [AnyNodeDescription],
13281
step: AnimationStep) -> [AnyNodeDescription] {
133-
82+
13483
return descriptions.map { (item: AnyNodeDescription) -> AnyNodeDescription in
13584
let itemReplaceKey = item.replaceKey
13685
let index = targetChildren.index { $0.replaceKey == itemReplaceKey }
13786
var item = item
138-
87+
13988
if index == nil {
14089
// the item is missing in the comparison, update it
14190
item = self.updatedDescription(for: item, using: childrenAnimation, step: step)
14291
}
143-
92+
14493
if var propsWithChildren = item.anyProps as? Childrenable {
14594
// the item has children, let's manage also the children
14695
let children = propsWithChildren.children
14796
let target = (item as? AnyNodeDescriptionWithChildren).flatMap({ $0.children })
148-
97+
14998
propsWithChildren.children = updatedDescriptions(
15099
for: children,
151100
using: childrenAnimation,
152101
targetChildren: target ?? [],
153102
step: step
154103
)
155-
104+
156105
return type(of: item).init(anyProps: propsWithChildren as! AnyNodeDescriptionProps)
157106
}
158-
107+
159108
// nothing to do
160109
return item
161110
}
162111
}
163-
112+
164113
/**
165114
Updates the description using an instance of `ChildrenAnimations`.
166115

167116
- parameter description: the original description
168117
- parameter childrenAnimation: the animations to use
169118
- parameter step: the step of the process
170119
- returns: the updated description
171-
*/
120+
*/
172121
private static func updatedDescription(
173122
for description: AnyNodeDescription,
174123
using childrenAnimation: AnyChildrenAnimations,
175124
step: AnimationStep) -> AnyNodeDescription {
176-
125+
177126
let animation = childrenAnimation[description]
178127
let transformers = step == .firstIntermediate ? animation.entryTransformers : animation.leaveTransformers
179-
128+
180129
let newProps = transformers.reduce(description.anyProps, { (props, transformer) -> AnyNodeDescriptionProps in
181130
return transformer(props)
182131
})
183-
132+
184133
return type(of: description).init(anyProps: newProps)
185134
}
135+
136+
/**
137+
Merge two `AnyNodeDescription` arrays. The first array has the priority
138+
if two elements are the same. The method will propagate the merge to also
139+
the children of the node descriptions, if the element implements
140+
the `Childrenable` protocol
141+
142+
- parameter firstArray: the first array
143+
- parameter secondArray: the second array
144+
- returns: the merged array
145+
*/
146+
private static func mergedDescriptions(
147+
_ firstArray: [AnyNodeDescription],
148+
with secondArray: [AnyNodeDescription]) -> [AnyNodeDescription] {
149+
150+
// lookup for first array
151+
var firstArrayLookup = [Int: Int]()
152+
for (index, item) in firstArray.enumerated() {
153+
firstArrayLookup[item.replaceKey] = index
154+
}
155+
156+
// lookup for second array
157+
var secondArrayLookup = [Int: Int]()
158+
for (index, item) in secondArray.enumerated() {
159+
secondArrayLookup[item.replaceKey] = index
160+
}
161+
162+
var result = firstArray
163+
var added = 0
164+
var firstArrayMaxPosition = -1
165+
166+
for item in secondArray {
167+
let position = firstArrayLookup[item.replaceKey]
168+
169+
if let position = position {
170+
// we already have the item
171+
firstArrayMaxPosition = max(firstArrayMaxPosition, position)
172+
continue
173+
}
174+
175+
result.insert(item, at: added + firstArrayMaxPosition + 1)
176+
added = added + 1
177+
}
178+
179+
// merge also children, if needed
180+
result = result.map { description in
181+
guard var propsWithChildren = description.anyProps as? Childrenable else {
182+
return description
183+
}
184+
185+
let secondItemChildren = secondArrayLookup[description.replaceKey]
186+
.flatMap({ secondArray[$0] })
187+
.flatMap({ $0 as? AnyNodeDescriptionWithChildren })
188+
.flatMap({ $0.children })
189+
190+
propsWithChildren.children = self.mergedDescriptions(propsWithChildren.children, with: secondItemChildren ?? [])
191+
return type(of: description).init(anyProps: propsWithChildren as! AnyNodeDescriptionProps)
192+
}
193+
194+
return result
195+
}
186196
}

Katana/Store/ActionWithSideEffect.swift

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,34 @@ import Foundation
1414

1515
A side effect is nothing more than a piece of code that can interact with external
1616
services or APIs (e.g., make a network request, get information from the disk and so on).
17-
Side effects are needed because the `updateState(currentState:)` function (which is the only other operation
17+
Side effects are needed because the `updatedState(currentState:)` function (which is the only other operation
1818
that is performed when an action is dispatched) must be pure and therefore it cannot
1919
interact with disk, network and so on.
2020

2121
### Dependencies
22-
You can see from the `sideEffect(state:dispatch:dependencies:)` signature that
22+
You can see from the `sideEffect(currentState:previousState:dispatch:dependencies:)` signature that
2323
a side effect takes as input some dependencies. This is a form of dependency injection
2424
for the side effects. By using only methods coming from the dependencies (instead of relying on
2525
global imports), testing is much more easier since you can inject a mocked version
2626
of the things you need in the side effect. For example, in a test, you may want to
2727
inject a mocked version of the class that manages the API requests, in order to control
2828
the result of the network call.
29-
30-
Every time a side effect is triggered, the dependencies (or better, the `SideEffectDependencyContainer`)
31-
is instantiated from scratch. We do this to avoid that pieces of state are saved in the managers or
32-
in the classes that there are in the dependencies, since we want to store all the relevant
33-
information in the `Store`
3429
*/
3530
public protocol ActionWithSideEffect: Action {
3631
/**
3732
Performs the side effect. This method is invoked when the action is dispatched,
38-
before it goes in the `updateState(currentState:action:)` function.
33+
after the `updateState(currentState:action:)` function.
3934

40-
- parameter state: the current state
41-
- parameter dispatch: a closure that can be used to dispatch new actions
42-
- parameter dependencies: the dependencies of the side effect
35+
- parameter currentState: the current state. The one returned by the
36+
`updateState(currentState:action:)` method
37+
- parameter previousState: the state of the store before the `updateState(currentState:action:)`
38+
invokation
39+
- parameter dispatch: a closure that can be used to dispatch new actions
40+
- parameter dependencies: the dependencies of the side effect
4341
*/
4442
func sideEffect(
45-
state: State,
43+
currentState: State,
44+
previousState: State,
4645
dispatch: @escaping StoreDispatch,
4746
dependencies: SideEffectDependencyContainer
4847
)

0 commit comments

Comments
 (0)