Redux has gained immense popularity for managing application state in a predictable and maintainable manner. This comprehensive guide walks through the process of implementing Redux in an iOS app, empowering developers to build scalable and efficient applications. From understanding core principles to practical implementation, this guide serves as a robust resource for adopting Redux in iOS development : the core principles - single source of truth, state immutability, and unidirectional data flow, where: Understanding Redux Architecture Store: Define the single source of truth for the application state Actions: Describe events that trigger state changes by reducer and trigger middleware Reducers: Pure functions that handle actions and update the state Middlewares: Interaction with services and side effects and the result of a new action . Create a new iOS project. Initialize the project structure, including folders Core, Flow, MW, Store, Services, where: Setting Up the Project Core for the basic architecture components and all additional general utilities Flow for UI components Views and their Service something like Controller/Presenter/View Model MW for Middlewares, DTO’s Store for Actions, Reducers, State Service for all components that are connected to the outside world something like File Manager/DB/Network/… . At first make protocol for actions and at the same time include reducer. Define actions that represent events triggering state changes. Organize actions into separate files based on functionality. Defining Actions Implementing Reducers. Create reducers that handle actions and update the state immutably. Combine reducers to manage different parts of the state protocol Action { associatedtype StateType: Equatable func reduce(_ state: inout StateType) } enum MainAction: Action { case user(UserAction) func reduce(_ state: inout MainState) { switch self { case let .user(action): action.reduce(&state) } } } enum UserAction: Action { case getUserData case receiveUser(UserModel) func reduce(_ state: inout MainState) { switch self { case .getUserData: state.user = .loading case let .receiveUser(model): state.user = .result(model) } } } . Make base elements and protocol for Middleware. Implement middleware to manage asynchronous actions. And create DTO with mapping to State model Managing Asynchronous Operations struct Box<Act> where Act: Action { let run: (@escaping (Act) -> Void) -> Void init(_ run: @escaping (@escaping (Act) -> Void) -> Void) { self.run = run } } extension Box { static func throwTask( _ run: @escaping (@escaping (Act) -> Void) async throws -> Void, catchError: @escaping (@escaping (Act) -> Void, Error) -> Void = { _, _ in } ) -> Self { Self { dispatcher in Task { do { try await run(dispatcher) } catch { catchError(dispatcher, error) } } } } } extension Box { static var zero: Self { Self { _ in }} static func +=(lhs: inout Self, rhs: Self) { let tmp = lhs lhs = Self { dispatcher in tmp.run(dispatcher) rhs.run(dispatcher) } } } protocol Middleware { associatedtype Act where Act: Action func run(state: Act.StateType, action: Act) -> Box<Act> } extension Middleware { var asAnyMiddleware: AnyMiddleware<Act> { AnyMiddleware(self.run(state:action:)) } } struct AnyMiddleware<Act>: Middleware where Act: Action { private let runer: (Act.StateType, Act) -> Box<Act> init(_ run: @escaping (Act.StateType, Act) -> Box<Act>) { self.runer = run } func run(state: Act.StateType, action: Act) -> Box<Act> { runer(state, action) } } struct UserMW: Middleware { private let net: UserNetProvider init(net: UserNetProvider) { self.net = net } func run(state: MainState, action: MainAction) -> Box<MainAction> { switch action { case let .user(userAction): return handleUser(state: state.user, action: userAction) default: return .zero } } func handleUser(state: ModelWrapper<UserModel>, action: UserAction) -> Box<MainAction> { switch action { case .getUserData: return Box.throwTask { dispatcher in let userDTO: UserDTO = try await net.getUserData() dispatcher(.user(.receiveUser(userDTO.convert))) } catchError: { dispatcher, error in // Handle error } default: return .zero } } } struct UserDTO: Decodable { let name: String let age: Int var convert: UserModel { UserModel(name: name, age: age) } } . Create State and Model wrapper for implementing all states. Create a store to manage the application state. Define the initial state and configure the store. Implementing the Store Of course Store implementation is very simple without corner cases enum ModelWrapper<Model> where Model: Equatable { case initialize case loading case error(Error) case result(Model) } extension ModelWrapper: Equatable { private var equatabeValue: String { return switch self { case .initialize: "initialize \(String(describing: Model.self))" case .loading: "loading \(String(describing: Model.self))" case let .error(error): error.localizedDescription case let .result(model): String(describing: model) } } static func == (lhs: ModelWrapper<Model>, rhs: ModelWrapper<Model>) -> Bool { lhs.equatabeValue == rhs.equatabeValue } } struct MainState: Equatable { public internal(set) var user: ModelWrapper<UserModel> = .initialize } struct UserModel: Equatable { let name: String let age: Int } actor Store<Act> where Act: Action { nonisolated var statePublisher: AnyPublisher<Act.StateType, Never> { stateSubject.eraseToAnyPublisher() } private var state: Act.StateType { didSet { stateSubject.send(state) } } private let stateSubject: CurrentValueSubject<Act.StateType, Never> private let middlewares: [AnyMiddleware<Act>] init(initState: Act.StateType, middlewares: [AnyMiddleware<Act>]) { self.state = initState self.stateSubject = CurrentValueSubject(initState) self.middlewares = middlewares } func dispatch(action: Act) { var box = Box<Act>.zero for mw in middlewares { box += mw.run(state: state, action: action) } action.reduce(&state) box.run(dispatch(action:)) } } . Integrate Redux with view components to read state from the store. Create simple View with ViewController and dispatch actions to trigger state updates. Connecting Components to Redux class ReduxVC<Act>: UIViewController where Act: Action { private let store: Store<Act> private var box = Set<AnyCancellable>() init(store: Store<Act>) { self.store = store super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func sink<S: Equatable>(map: KeyPath<Act.StateType, S>, _ sink: @escaping (_ storeState: S) -> Void) { store .statePublisher .map(map) .removeDuplicates() .receive(on: DispatchQueue.main) .sink(receiveValue: sink) .store(in: &box) } func dispatch(_ action: Act) { Task { await store.dispatch(action: action) } } } final class UserScreenView: UIView { private let spiner = { $0.isHidden = true $0.color = .black $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UIActivityIndicatorView(style: .medium)) private let dataView = { $0.isHidden = true $0.backgroundColor = .lightGray $0.clipsToBounds = true $0.layer.cornerRadius = 10 $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UIView()) private let errorView = { $0.isHidden = true $0.backgroundColor = .lightGray $0.clipsToBounds = true $0.layer.cornerRadius = 10 $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UIView()) private let nameLabel = { $0.font = .systemFont(ofSize: 22, weight: .regular) $0.textColor = .black $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UILabel()) private let ageLabel = { $0.font = .systemFont(ofSize: 22, weight: .regular) $0.textColor = .black $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UILabel()) private let errorLabel = { $0.font = .systemFont(ofSize: 22, weight: .regular) $0.textColor = .systemRed $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UILabel()) init() { super.init(frame: .zero) backgroundColor = .white addSubview(spiner) addSubview(dataView) dataView.addSubview(nameLabel) dataView.addSubview(ageLabel) addSubview(errorView) errorView.addSubview(errorLabel) NSLayoutConstraint.activate([ spiner.centerXAnchor.constraint(equalTo: centerXAnchor), spiner.centerYAnchor.constraint(equalTo: centerYAnchor), spiner.heightAnchor.constraint(equalToConstant: 30), spiner.widthAnchor.constraint(equalToConstant: 30), dataView.centerXAnchor.constraint(equalTo: centerXAnchor), dataView.centerYAnchor.constraint(equalTo: centerYAnchor), nameLabel.leadingAnchor.constraint(equalTo: dataView.leadingAnchor, constant: 22), nameLabel.trailingAnchor.constraint(equalTo: dataView.trailingAnchor, constant: -22), nameLabel.topAnchor.constraint(equalTo: dataView.topAnchor, constant: 22), ageLabel.widthAnchor.constraint(equalTo: nameLabel.widthAnchor), ageLabel.centerXAnchor.constraint(equalTo: dataView.centerXAnchor), ageLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 22), ageLabel.bottomAnchor.constraint(equalTo: dataView.bottomAnchor, constant: -22), errorView.centerXAnchor.constraint(equalTo: centerXAnchor), errorView.centerYAnchor.constraint(equalTo: centerYAnchor), errorLabel.leadingAnchor.constraint(equalTo: errorView.leadingAnchor, constant: 22), errorLabel.trailingAnchor.constraint(equalTo: errorView.trailingAnchor, constant: -22), errorLabel.topAnchor.constraint(equalTo: errorView.topAnchor, constant: 22), errorLabel.bottomAnchor.constraint(equalTo: errorView.bottomAnchor, constant: -22) ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func loadingData() { dataView.isHidden = true errorView.isHidden = true spiner.isHidden = false spiner.startAnimating() } func errorSetup(_ error: String) { dataView.isHidden = true spiner.isHidden = true errorView.isHidden = false spiner.stopAnimating() errorLabel.text = error } func dataSetup(_ model: UserModel) { dataView.isHidden = false spiner.isHidden = true errorView.isHidden = true spiner.stopAnimating() nameLabel.text = model.name ageLabel.text = String(model.age) } } final class UserScreenVC: ReduxVC<MainAction> { let mainView = UserScreenView() override func loadView() { self.view = mainView } override func viewDidLoad() { super.viewDidLoad() sink(map: \.user) { [unowned self] state in switch state { case .initialize: break case .loading: mainView.loadingData() case let .error(err): mainView.errorSetup(err.localizedDescription) case let .result(model): mainView.dataSetup(model) } } dispatch(.user(.getUserData)) } } And finally assembly all components together into SceneDelegate guard let windowScene = (scene as? UIWindowScene) else { return } let netProvider = NetProvider() let store = Store<MainAction>( initState: MainState(), middlewares: [ UserMW(net: UserNetProvider(netProvider: netProvider)).asAnyMiddleware ] ) window = UIWindow(frame: UIScreen.main.bounds) window?.windowScene = windowScene window?.rootViewController = UserScreenVC(store: store) window?.makeKeyAndVisible() All structural folders Core, Flow, MW, Store, Services you can separate into separate elements e.g. Frameworks/Swift Packages/Libraries or something like this Conclusion: In conclusion, the adoption of Redux architecture in iOS app development offers a structured, scalable, and efficient approach to managing application state. This comprehensive guide has illuminated the core principles of Redux, emphasizing the importance of a single source of truth, state immutability, and unidirectional data flow. The step-by-step exploration of implementing Redux architecture has covered various key components