Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/ReduxExample.xcworkspace/xcuserdata/alessandromartin.xcuserdatad/UserInterfaceState.xcuserstate
/ReduxExample.xcodeproj/xcuserdata/alessandromartin.xcuserdatad/xcschemes/xcschememanagement.plist
/Pods/Pods.xcodeproj/xcuserdata/alessandromartin.xcuserdatad/xcschemes/xcschememanagement.plist
/ReduxExample.xcworkspace/xcuserdata/alessandromartin.xcuserdatad
12 changes: 12 additions & 0 deletions ReduxExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
1CF6803220692DA30022C9E7 /* ReduxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF6803120692DA30022C9E7 /* ReduxTests.swift */; };
1CF6803920692DC30022C9E7 /* Redux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF6802720687A000022C9E7 /* Redux.swift */; };
895901BBDECC2146CC7DEE8E /* Pods_ReduxTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36947F3EFFA34BEFF75C6EDA /* Pods_ReduxTests.framework */; };
BF6A98DA2069B5D700B4D05E /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6A98D92069B5D700B4D05E /* AppState.swift */; };
BF6A98DC2069B5FB00B4D05E /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6A98DB2069B5FB00B4D05E /* User.swift */; };
BF6A98DE2069B65A00B4D05E /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6A98DD2069B65A00B4D05E /* AppConfiguration.swift */; };
FDDE9FCDBEA4910EB4969E60 /* Pods_ReduxExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0F7A592F2BBBDB070D9E1D6 /* Pods_ReduxExample.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -44,6 +47,9 @@
36947F3EFFA34BEFF75C6EDA /* Pods_ReduxTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReduxTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
63E6A6F89869742E0F68463B /* Pods-ReduxExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReduxExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReduxExample/Pods-ReduxExample.release.xcconfig"; sourceTree = "<group>"; };
8C3A44712CC3EA2BA6F64619 /* Pods-ReduxTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReduxTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReduxTests/Pods-ReduxTests.release.xcconfig"; sourceTree = "<group>"; };
BF6A98D92069B5D700B4D05E /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
BF6A98DB2069B5FB00B4D05E /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
BF6A98DD2069B65A00B4D05E /* AppConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = "<group>"; };
D12C02EA05B7841D17CB7E14 /* Pods-ReduxTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReduxTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReduxTests/Pods-ReduxTests.debug.xcconfig"; sourceTree = "<group>"; };
DC904AC289E2A89F33F26855 /* Pods-ReduxExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReduxExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReduxExample/Pods-ReduxExample.debug.xcconfig"; sourceTree = "<group>"; };
F0F7A592F2BBBDB070D9E1D6 /* Pods_ReduxExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReduxExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -99,6 +105,9 @@
1CF6801C2066AF840022C9E7 /* LaunchScreen.storyboard */,
1CF6801F2066AF840022C9E7 /* Info.plist */,
1CF6802720687A000022C9E7 /* Redux.swift */,
BF6A98DD2069B65A00B4D05E /* AppConfiguration.swift */,
BF6A98D92069B5D700B4D05E /* AppState.swift */,
BF6A98DB2069B5FB00B4D05E /* User.swift */,
);
path = ReduxExample;
sourceTree = "<group>";
Expand Down Expand Up @@ -354,9 +363,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BF6A98DE2069B65A00B4D05E /* AppConfiguration.swift in Sources */,
1CF680162066AF840022C9E7 /* ViewController.swift in Sources */,
BF6A98DC2069B5FB00B4D05E /* User.swift in Sources */,
1CF680142066AF840022C9E7 /* AppDelegate.swift in Sources */,
1CF6802820687A000022C9E7 /* Redux.swift in Sources */,
BF6A98DA2069B5D700B4D05E /* AppState.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
74 changes: 74 additions & 0 deletions ReduxExample/AppConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// AppConfiguration.swift
// ReduxExample
//
// Created by Alessandro Martin on 03/27/2018.
// Copyright © 2018 Jonathan Cravotta. All rights reserved.
//

import Foundation

// Note: equatable conformance for types like this should be automatic in Swift 4.1
struct AppConfiguration: Equatable {
var arePushNotificationsEnabled: Bool
var isDarkThemed: Bool

static let `default` = AppConfiguration(arePushNotificationsEnabled: false, isDarkThemed: false)

static func ==(lhs: AppConfiguration, rhs: AppConfiguration) -> Bool {
return lhs.arePushNotificationsEnabled == rhs.arePushNotificationsEnabled
&& lhs.isDarkThemed == rhs.isDarkThemed
}
}

//MARK: - Reducer
func appConfigurationReducer(_ action: Action, _ state: AppConfiguration) -> AppConfiguration {
var newState = state

switch action {
case let action as TogglePushNotifications:
newState.arePushNotificationsEnabled = action.enable
case let action as ToggleDarkTheme:
newState.isDarkThemed = action.enable
case is LoadAppConfiguration:
newState = loadAppConfiguration()
case is SaveAppConfiguration:
saveAppConfiguration(newState)
default:
break
}

return newState
}

//MARK: - Actions
struct TogglePushNotifications: Action {
let enable: Bool
}

struct ToggleDarkTheme: Action {
let enable: Bool
}

struct LoadAppConfiguration: Action {}
private func loadAppConfiguration(from defaults: UserDefaults = .standard) -> AppConfiguration {
let pushNotificationsEnabled = defaults.bool(forKey: .pushNotificationsKey)
let darkThemeEnabled = defaults.bool(forKey: .darkThemeKey)
print("Configuration loaded!")

return AppConfiguration(arePushNotificationsEnabled: pushNotificationsEnabled,
isDarkThemed: darkThemeEnabled)
}

struct SaveAppConfiguration: Action {}
private func saveAppConfiguration(_ appConfiguration: AppConfiguration, to defaults: UserDefaults = .standard) {
defaults.set(appConfiguration.arePushNotificationsEnabled, forKey: .pushNotificationsKey)
defaults.set(appConfiguration.isDarkThemed, forKey: .darkThemeKey)
defaults.synchronize()
print("Configuration saved!")
}

private extension String {
static let pushNotificationsKey = "PushNotificationsEnabled"
static let darkThemeKey = "DarkThemeEnabled"
}
14 changes: 11 additions & 3 deletions ReduxExample/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@

import UIKit

// It's a var so it can be replaced and the app state fully reset
var store = Store<AppState>(reducer: appReducer, state: .initial)

func state() -> AppState {
return store.state.value
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.

store.dispatch(LoadAppConfiguration())

return true
}

Expand All @@ -27,6 +36,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
store.dispatch(SaveAppConfiguration())
}

func applicationWillEnterForeground(_ application: UIApplication) {
Expand All @@ -40,7 +50,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}


}

20 changes: 20 additions & 0 deletions ReduxExample/AppState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// AppState.swift
// ReduxExample
//
// Created by Alessandro Martin on 03/27/2018.
// Copyright © 2018 Jonathan Cravotta. All rights reserved.
//

struct AppState: StateType {
let user: User
let appConfiguration: AppConfiguration

static let initial = AppState(user: .anonymous, appConfiguration: .default)
}

//MARK: - Reducer
func appReducer(_ action: Action, _ state: AppState) -> AppState {
return AppState(user: userReducer(action, state.user),
appConfiguration: appConfigurationReducer(action, state.appConfiguration))
}
93 changes: 32 additions & 61 deletions ReduxExample/Redux.swift
Original file line number Diff line number Diff line change
@@ -1,82 +1,53 @@
//
// GenericRedux.swift
// Redux.swift
// ReduxExample
//
// Created by Jonathan Cravotta on 3/25/18.
// Copyright © 2018 Jonathan Cravotta. All rights reserved.
//

import Foundation
import ReactiveSwift

protocol Store {
associatedtype State
associatedtype Action

var state: MutableProperty<State> { get }
func reducer(_ action: Action, _ state: State) -> State
}
protocol StateType {}
protocol Action {}

extension Store {
func dispatch(action: Action) {
state.value = reducer(action, state.value)
}
}
typealias Reducer<ReducerStateType> = (Action, ReducerStateType) -> ReducerStateType

// Example:
struct User {
var name: String
var zipcode: Int
var sizes: [Int]
}
final class Store<State: StateType> {

enum UserAction {
case updateName(String)
case updateZip(Int)
case updateSizes([Int])
case updateZipAndSizes(zip: Int, sizes: [Int])
}

class UserStore: Store {

typealias Action = UserAction
typealias State = User

var state: MutableProperty<User>
let reducer: Reducer<State>
let state: MutableProperty<State>

init(user: User) {
self.state = MutableProperty(user)
}

func reducer(_ action: Action, _ state: User) -> User {
var newState = state

switch action {
case .updateName(let name): newState.name = name
case .updateZip(let zip): newState.zipcode = zip
case .updateSizes(let sizes): newState.sizes = sizes

case let .updateZipAndSizes(zip, sizes):
newState.zipcode = zip
newState.sizes = sizes
}

return newState
init(reducer: @escaping Reducer<State>, state: State) {
self.reducer = reducer
self.state = MutableProperty(state)
}
}

func dispatch(_ action: Action) {
state.value = reducer(action, state.value)
}

//:D
extension MutableProperty {
@discardableResult
public func observeProducer(_ value: @escaping (MutableProperty.Value) -> Void) -> Disposable {
return producer.startWithValues(value)
public func observeSignal<Part: Equatable>(keyPath: KeyPath<State,Part>, action: @escaping (Part) -> Void) -> Disposable? {
return state.signal.combinePrevious().observeValues { previous, current in
let previousPart = previous[keyPath: keyPath]
let currentPart = current[keyPath: keyPath]

if previousPart != currentPart {
action(currentPart)
}
}
}

@discardableResult
public func observeSignal(_ value: @escaping (MutableProperty.Value) -> Void) -> Disposable? {
return signal.observeValues(value)
public func observeProducer<Part: Equatable>(keyPath: KeyPath<State,Part>, action: @escaping (Part) -> Void) -> Disposable {
return state.producer.combinePrevious().startWithValues { previous, current in
let previousPart = previous[keyPath: keyPath]
let currentPart = current[keyPath: keyPath]

if previousPart != currentPart {
action(currentPart)
}
}
}
}


63 changes: 63 additions & 0 deletions ReduxExample/User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// User.swift
// ReduxExample
//
// Created by Alessandro Martin on 03/27/2018.
// Copyright © 2018 Jonathan Cravotta. All rights reserved.
//

struct User {
var name: String
var zipcode: Int
var sizes: [Int]

static let anonymous = User(name: "Anonymous", zipcode: 90210, sizes: [])
}

// Note: equatable conformance for types like this should be automatic in Swift 4.1
extension User: Equatable {
static func ==(lhs: User, rhs: User) -> Bool {
return lhs.name == rhs.name
&& lhs.zipcode == rhs.zipcode
&& lhs.sizes == rhs.sizes
}
}

//MARK: - Reducer
func userReducer(_ action: Action, _ state: User) -> User {
var newState = state

switch action {
case let action as UserName:
newState.name = action.name
case let action as UserZipCode:
newState.zipcode = action.zipcode
case let action as UserSizes:
newState.sizes = action.sizes
case let action as UserZipCodeAndSizes:
newState.zipcode = action.zipcode
newState.sizes = action.sizes
default:
break
}

return newState
}

//MARK: - Actions
struct UserName: Action {
let name: String
}

struct UserZipCode: Action {
let zipcode: Int
}

struct UserSizes: Action {
let sizes: [Int]
}

struct UserZipCodeAndSizes: Action{
let zipcode: Int
let sizes: [Int]
}
Loading