├── .gitignore ├── Assets ├── Chapter-03-1.png ├── Chapter-03-2.png ├── Chapter-06-1.png ├── Chapter-06-2.png ├── Chapter-08-1.png ├── Chapter-08-2.png ├── Chapter-08-3.png ├── Chapter-08-4.png ├── Chapter-08-5.png ├── Chapter-08-6.png ├── Chapter-08-7.png ├── Chapter-08-8.png ├── Chapter-10-1.png ├── Chapter-10-2.png ├── Chapter-12-1.png ├── Chapter-12-2.png ├── Chapter-12-3.png ├── Chapter-12-4.png ├── Chapter-19-1.png ├── Chapter-19-2.png ├── Chapter-19-3.png ├── Chapter-19-4.png ├── Chapter-19-5.png └── Chapter-19-6.png ├── Chapters ├── Chapter-00.md ├── Chapter-01.md ├── Chapter-02.md ├── Chapter-03.md ├── Chapter-04.md ├── Chapter-05.md ├── Chapter-06.md ├── Chapter-07.md ├── Chapter-08.md ├── Chapter-09.md ├── Chapter-10.md ├── Chapter-11.md ├── Chapter-12.md ├── Chapter-13.md ├── Chapter-14.md ├── Chapter-15.md ├── Chapter-16.md ├── Chapter-17.md ├── Chapter-18.md ├── Chapter-19.md └── Chapter-20.md ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Assets/Chapter-03-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-03-1.png -------------------------------------------------------------------------------- /Assets/Chapter-03-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-03-2.png -------------------------------------------------------------------------------- /Assets/Chapter-06-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-06-1.png -------------------------------------------------------------------------------- /Assets/Chapter-06-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-06-2.png -------------------------------------------------------------------------------- /Assets/Chapter-08-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-1.png -------------------------------------------------------------------------------- /Assets/Chapter-08-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-2.png -------------------------------------------------------------------------------- /Assets/Chapter-08-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-3.png -------------------------------------------------------------------------------- /Assets/Chapter-08-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-4.png -------------------------------------------------------------------------------- /Assets/Chapter-08-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-5.png -------------------------------------------------------------------------------- /Assets/Chapter-08-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-6.png -------------------------------------------------------------------------------- /Assets/Chapter-08-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-7.png -------------------------------------------------------------------------------- /Assets/Chapter-08-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-08-8.png -------------------------------------------------------------------------------- /Assets/Chapter-10-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-10-1.png -------------------------------------------------------------------------------- /Assets/Chapter-10-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-10-2.png -------------------------------------------------------------------------------- /Assets/Chapter-12-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-12-1.png -------------------------------------------------------------------------------- /Assets/Chapter-12-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-12-2.png -------------------------------------------------------------------------------- /Assets/Chapter-12-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-12-3.png -------------------------------------------------------------------------------- /Assets/Chapter-12-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-12-4.png -------------------------------------------------------------------------------- /Assets/Chapter-19-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-19-1.png -------------------------------------------------------------------------------- /Assets/Chapter-19-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-19-2.png -------------------------------------------------------------------------------- /Assets/Chapter-19-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-19-3.png -------------------------------------------------------------------------------- /Assets/Chapter-19-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-19-4.png -------------------------------------------------------------------------------- /Assets/Chapter-19-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-19-5.png -------------------------------------------------------------------------------- /Assets/Chapter-19-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swift-ImmutableData/ImmutableData-Book/cac5528d565c8a2fb85c9d90d16a9de76fa93618/Assets/Chapter-19-6.png -------------------------------------------------------------------------------- /Chapters/Chapter-00.md: -------------------------------------------------------------------------------- 1 | # Flux, Redux, and SwiftUI 2 | 3 | Before we dive into some code, it’s important to begin with some background, philosophy, and history. What did product engineering look like for the Apple Ecosystem before SwiftUI? How did other declarative UI frameworks and ecosystems manage shared mutable state at scale? What could we learn from other ecosystems that could influence how we think about our shared mutable state when building products for SwiftUI? It’s important to understand that the architecture we are proposing was not built in a vacuum. The code *itself* will be new, but the ideas *behind* the code have already proven themselves in the real world. 4 | 5 | ## React 6 | 7 | Facebook launched in 2004.[^1] By 2010, FB had grown to 500 million users.[^2] The previous year, FB reached another important milestone: profitability.[^3] To understand more the scale that engineers were shipping impact at during this era, FB was employing less than 500 engineers. 8 | 9 | Historically, FB hired engineers as software generalists. Engineers were onboarded without being preallocated into one specific team or role. After onboarding, engineers quickly discovered that nothing was ever “complete” at FB. The company and products grew so quickly, engineers were encouraged — and were needed — to work on new products outside their speciality. 10 | 11 | As FB (the company and the product) scaled, engineers building for front-end WWW began to realize their architecture was not scaling with them. At this point in time, FB front-end architecture was built on “classic” front-end engineering. Engineers used imperative logic to manipulate long-lived mutable view objects. Sometimes this was called “MVC”, sometimes it was called “MVVM”, and sometimes it was called “MVW” (Model-View-Whatever) or “Model-View-Asterisk”. These architectures all shared a common assumption that made it challenging to onboard new engineers and ship products at scale: views were mutable objects and engineers needed to use complex imperative logic to manage their state correctly. 12 | 13 | Starting around 2011, a FB engineer named Jordan Walke began to “rethink best practices” about application architecture for front-end engineering. This new framework would become ReactJS. React gave engineers the tools to *declare* their view component tree without the need for *imperative* mutations directly on mutable view objects. React gave product engineers the tools to focus on “the what not the how”. For two years, FB products began to migrate to this new infra. React led to code that was faster to write and easier to maintain. After crossing the threshold of one-billion users in 2012,[^4] FB announced the ReactJS framework would be released to the community as open source in 2013.[^5] 14 | 15 | The early public demos of React gave engineers a battle-tested infra for declaring their user interface, but FB did not publicly make a strong value statement about how these engineers should manage shared mutable state at scale. While a declarative framework like React redefines the “Controller-View” relationship, React — as announced to the public — did not yet have a strong opinion about how to redefine the “Model-Controller” relationship. 16 | 17 | ## Flux 18 | 19 | What engineers outside FB did not know was that FB *did* have a new application architecture being developed internally alongside the React infra. During the two years React was being developed and scaled, a team of FB engineers led by Jing Chen also noticed that their architecture was not scaling as the company was growing. React focused its attention on redefining the programming model engineers use to build graphs of view components; this new team of engineers began to think about their data and their models with a similar philosophy. 20 | 21 | At this time, conventional architectures were encouraging complex mutable state to be delivered to views through mutable objects: controllers or view models. Views were using imperative logic to mutate shared state directly: through a controller or on the model objects themselves. As the size of the product grew, the graph of relationships between controllers and models grew quadratically: it was out of control. One controller class would become so large it was slowing down engineers that needed to work on it. A temporary solution might have been to break this apart into “child” controllers, but the relationship graph between these controllers then grew quadratically. There was always going to be quadratic complexity; trying to refactor it out of one place just moved it somewhere else. 22 | 23 | In addition, mutable model objects — where the state of these models could be directly mutated with imperative logic by the view objects — led to code that was difficult to reason about; as the size of the product scaled, engineers needed to know “global” context to make “local” changes. It was very easy for bugs to ship; an engineer might mutate state at some place in the view graph when another place in the view graph was not expecting, or prepared for, a mutation. 24 | 25 | The next problem was bugs that looked like “race conditions” or “non-deterministic” chain reactions. Since mutable state was often being bound with two-directional data bindings that can both read from and write to an object, an engineer might mutate shared state in one part of their view graph, while a different part of the view graph is not only subscribing to listen for state mutations, but then making their own state mutations once they received that notification. 26 | 27 | Code was complex, unpredictable, and difficult to make changes to. Parallel to the work the React team was doing to redefine the programming model product engineers used to build user interfaces, this new team began to redefine the programming model product engineers used to manage shared mutable state. 28 | 29 | The architecture was called Flux, and shared a lot of philosophy with React. Flux gave product engineers tools to think about shared mutable state by moving away from an imperative programming model. Product engineers could *declare* actions and events as they happen from their view components and migrate the imperative logic to mutate state down into their model layer in Flux Stores. Product engineers no longer needed two-directional data bindings that read and wrote to shared state; the data flow became one-directional. Complex view controllers — or complex graphs of view controllers — started to become obsolete. Code became simple, predictable, and easy to make changes to. 30 | 31 | Flux was built on JS, but drew a lot of influence from Haskell, a functional programming language, and Ruby, a “multi-paradigm” language (like JavaScript) that also encouraged functional programming patterns. Another influence was the Command Query Responsibility Segregation (CQRS) Pattern.[^6] 32 | 33 | Conceptually and ideologically, Flux paired very well with the programming model of React. In 2014, one year after React was announced, FB announced the Flux architecture.[^7] While Flux did ship with a small JS framework library, product engineers outside FB were encouraged to think about Flux as a design pattern that could also be implemented with custom frameworks. While the first public releases of React shipped without a strong opinion about an architecture for state management, FB was now evangelizing Flux as the correct “default” choice for most product engineers coming to the React Ecosystem. 34 | 35 | ## ComponentKit 36 | 37 | While front-end engineering for WWW was undergoing big changes at FB, mobile engineering for iOS was also evolving in new directions. From the early days of engineering at FB, the company was first and foremost a “web” company. The DNA of the company was very much tied into the WWW product and ecosystem. In an attempt to share engineering resources and knowledge, the FB “Big Blue” mobile app for iOS began to ship with many surfaces rendered in HTML5; it was a “hybrid app”. As the app began to grow (more products and more users), the performance limitations of HTML5 were impacting the reputation of the business: users were complaining. About the time WWW engineers were beginning to build React, FB engineers started to build a new version of the native mobile app for iOS. 38 | 39 | As FB transitioned away from HTML5 hybrid views, engineers made some architectural decisions that would have important consequences later. FB mobile engineers chose MVC and UIKit as their main architecture. To manage their shared mutable state and data, mobile engineers chose the Core Data framework. The first native rewrites to the Big Blue FB app were successful: performance was much better than the hybrid app. This same year, FB publicly announced it was focusing on “mobile-first” growth.[^8] Historically, engineers at FB might launch new features on WWW. Launching that same feature on mobile iOS either meant writing HTML5 in a hybrid app, or waiting for one of the (few) native specialists at the company to build native Objective-C and UIKit. 40 | 41 | With FB focusing on mobile-first growth, engineers from across the company that were shipping new products were now ramping up on learning UIKit and Core Data to ship on this new technology. Everything was good… until it wasn’t. 42 | 43 | When the engineers building the native rewrite chose MVC, UIKit, and Core Data, the engineers were choosing what looked like the “best practices” at the time. These were the tools Apple built, and these were the tools Apple told engineers were the best for building applications at scale. While building an application using “conventional” iOS architecture and frameworks might have helped FB move fast and ship quickly, this architecture would soon lead to the same class of problems that caused the WWW team to pivot to React and Flux. 44 | 45 | When placing and updating views on screen, the imperative and object-oriented programming model of UIKit and MVC was slowing engineers down. View Controllers were growing at quadratic complexity as the product scaled. Controllers — either one giant controller or a complex graph of controllers — would need to correctly position and mutate a graph of view objects using imperative logic. It was very easy for engineers to make a mistake that led to UI bugs and glitches. As this was happening, the Core Data framework was locking engineers into thinking about data as mutable model objects which were updated using imperative logic. Two-directional data bindings on these mutable model objects were leading to the same “cascading” class of bugs the front-end WWW team saw before Flux. On top of that, Core Data was really slow. Engineers tried all the tricks they could think of to speed up Core Data, but it led to unnecessary complexity that product engineers would have to work through and understand. 46 | 47 | Neither of these frameworks (UIKit or Core Data) were scaling to support the ambitious goals of FB continuing to ship products with mobile-first growth. After about two years of struggling with MVC, a team of engineers led by Adam Ernst began an ambitious attempt to “rewrite” the app that had already been rewritten only two years before. They saw that the front-end WWW team encountered problems scaling products built on a MVC architecture, and the native mobile team was encountering the same class of problems. They saw that migrating to React and Flux solved these problems for WWW engineers, and they began to write an Objective-C++ native version of the React and Flux frameworks. These new frameworks, like React and Flux, would encourage declarative thinking instead of imperative thinking, functional programming instead of object-oriented programming, and immutable model values instead of mutable model objects. 48 | 49 | The new UI framework was called ComponentKit.[^9] ComponentKit originally launched as a rewrite of the News Feed product, but ComponentKit spread to become the dominant framework product engineers would use for mobile iOS at FB. While ComponentKit was using the principles of React to solve the scalability problems of UI layout by migrating away from UIKit, a project called MemModels was in development to use the principles of Flux to solve the scalability problems of mutable state management by migrating away from Core Data. ComponentKit was released to the open-source community in 2015, but the “native” Flux framework was unfortunately not released publicly. Similar to the first version of React, FB presented a solution for bringing declarative programming to UI product engineering, but did not ship a companion framework for bringing declarative programming to complex state management. 50 | 51 | ## Redux 52 | 53 | The Flux framework and architecture was released to the open-source community in 2014. Over the following year, the engineering team behind Flux saw that product engineers at FB were beginning to repeat some of the same logic across products. Flux did not make assumptions about caching, faulting, paging, sorting, or other typical work that a network-driven application like FB might perform to fetch and present data from a remote server. For smaller companies and teams, the Flux framework might have been a great starting point for building the data model for smaller applications. For a rapidly growing company like FB, there was a lot of engineering impact that was being lost on duplicating logic across multiple product surfaces. The Flux team began to build a new framework which started with the philosophical foundation of Flux and offered new infra to help product teams working across FB that were blocked on these common problems. 54 | 55 | The new framework was called [Relay][^10], and was released along with the [GraphQL][^11] data query language as a Flux-Inspired solution for managing the complex state of a network-driven application driven by a complex (but well defined) graph schema of data. The Relay framework was powerful and a huge leap forward over the initial release of Flux, but Relay was not a lightweight and general-purpose solution for state management: it was dependent on GraphQL as a schema to model data. 56 | 57 | As FB engineers were building Relay, the React ecosystem and community continued to experiment with the Flux architecture. Over time, a number of legit grievances about decisions or ambiguity in the original Flux implementation led to the community beginning to think about what a next-generation evolution of Flux might look like.[^12] 58 | 59 | In 2015, Dan Abramov introduced the Redux framework and architecture at React Europe.[^13] For the most part, Redux began with many of the same opinions and assumptions of Flux: data still flowed in one direction with product engineers passing actions using declarative logic instead of mutating state directly at the component level with imperative logic. At the model layer, Flux Stores — which could be multiple stores in one application — contained imperative logic for mapping actions to mutations on state. Unlike Flux, Redux builds from just one store and saves engineers from managing complexity to keep multiple stores synchronized. The imperative logic to map actions to mutations on state is written outside of Stores in pure functions called Reducers. Using inspiration from Elm (a language and architecture emphasizing immutability), Redux Reducers map the state of an application with an action to produce the next state of the application.[^14] Using inspiration from ImmutableJS, Redux requires these state objects to be immutable — unlike Flux, which gave engineers the option to model their state with mutable objects.[^14] 60 | 61 | The Relay framework was a very powerful solution for managing network-driven applications built from GraphQL data schemas at scale. This project was hugely impactful inside FB and for applications built from a similar tech stack, but it was a “heavyweight” solution relative to the simplicity and flexibility of the original Flux implementation. Redux refined the original Flux implementation with ideas that reduced boilerplate and simplified state transformations. Leveraging immutable data structures led to code that was more predictable and easier to reason about. Over time, Redux became the dominant choice for unidirectional data flow state management for React Applications.[^15] 62 | 63 | ## SwiftUI 64 | 65 | While the WWW team at FB was building the React framework in JS and the iOS team at FB was building the ComponentKit framework in Objective-C++, engineers at Apple led by Chris Lattner were building the Swift Programming Language.[^16] Swift brought some influences from C++ along with some influences from Objective-C. Swift also brought some influences from the functional programming patterns found in languages and ecosystems like Haskell and Ruby — which were also big influences on Flux. 66 | 67 | One of the biggest differences between Swift and Objective-C was the flexibility and power of immutable value types. While simple C-style structs allocated on the stack were always available in Objective-C, the primary building blocks of almost all Objective-C applications were objects allocated on the heap. For an application like FB that was migrating away from the semantics of mutability, this led to workarounds like choosing Objective-C++ to improve the efficiency of creating objects and the Remodel library for adding “immutable” semantics to mutable data objects.[^17][^18] Swift offered more flexibility for engineers by shipping powerful immutable value type structures along with support for mutable reference type classes. Swift Structs were far more flexible and powerful than Objective-C structs. Because Swift Structs followed value semantics, engineers were now able to “reason locally” about their code in a way that was not possible with reference semantics.[^19] Apple soon began to recommend structs and value semantics as the “default” choice for engineers to model their data types.[^20] 68 | 69 | Throughout the Objective-C era, Apple continued to evangelize MVC as the preferred application architecture for applications built from AppKit and UIKit.[^21][^22] While Swift introduced powerful new abilities to encourage functional programming with immutable model values, the primary tools for building applications on Apple platforms were still AppKit and UIKit — which meant that the application architecture recommended by Apple continued to be object-oriented MVC.[^23][^24] 70 | 71 | As early as 2017, rumors began to leak that Apple was building a new framework for declarative UI.[^25] In 2019, Apple announced SwiftUI.[^26] For engineers experienced with building applications in the FB ecosystem using React and ComponentKit, the programming model used by SwiftUI looked very familiar. SwiftUI “views” — similar to what React and ComponentKit called “components” — were built declaratively using immutable data structures. Rather than product engineers telling their view hierarchy *how* it should be built using imperative logic, product engineers started telling the SwiftUI infra *what* should be built using declarative logic. Like React and ComponentKit, SwiftUI encouraged product engineers to focus on “the what not the how”. 72 | 73 | Considering the experience front-end teams from WWW and iOS had trying to scale classic MVC architectures across complex applications and large teams, a first-party solution for declarative UI built on a language that included support for immutable data values and functional programming looked like a huge leap forward for engineering on Apple Platforms. While the launch of SwiftUI offered a framework for managing graphs of view components declaratively, SwiftUI, like the early versions of React and ComponentKit, shipped without strong public opinions about what architecture should look like for mutable state management. 74 | 75 | The early demos of SwiftUI from Apple emphasize what React Engineers would think of as “component state”.[^27] While Apple was encouraging a unidirectional flow of data *through* one subgraph of view components, we did not yet hear very clear messaging from Apple about what architecture we would use for a unidirectional flow of data *across* multiple subgraphs of view components. Without a clear new direction from Apple, many engineers across the community — engineers that might not have the context of what had been happening in and around FB — began to architect SwiftUI applications by using declarative logic to put view components on screen, but falling back to imperative logic on shared mutable state to transform user input into the new state of their system.[^28] 76 | 77 | ## SwiftData 78 | 79 | In 2023, Apple launched a “next-generation” update to their Core Data framework. This new version was called SwiftData.[^29] SwiftData reduced some of the legacy artifacts, complex setup, and repetitive boilerplate code that was needed for many engineers using Core Data in modern Swift applications. What SwiftData did *not* offer product engineers was a fundamentally different programming model from what was already being offered in Core Data. When using SwiftData with SwiftUI, product engineers were still using imperative logic to mutate shared object references. The “UI” side of the application was modern and declarative, but the “Data” side of the application was still classic and imperative. 80 | 81 | ## ImmutableData 82 | 83 | As we build the `ImmutableData` infra and deploy the architecture to sample applications in SwiftUI, we will see how we can bring our mental model of “the what not the how” to a complete application architecture. With our experience building SwiftUI applications, we already know how to “think declaratively” for building a graph of view components; all we do now is complete the pattern across our “full stack”: UI *and* Data. As we build our sample applications, we will use declarative programming and a unidirectional data flow to demonstrate the same philosophies and patterns that scaled to one billion users across applications at FB and across the React ecosystem. 84 | 85 | Once our applications are built and we see for ourselves what this architecture looks like, we will benchmark and measure performance. We will see how this architecture built from immutable data structures instead of SwiftData will save memory and CPU. We will even see how the `ImmutableData` architecture can continue to leverage SwiftData for some of its specialized behaviors: offering the improved performance and programming model of `ImmutableData` as a “front end” along with the efficient persistent data storage of SwiftData as a “back end”. 86 | 87 | Migrating to `ImmutableData` might seem like we are asking you to “throw away” knowledge and experience, but we see a different point-of-view. We are asking you to expand the mental model you have already learned and practiced for “thinking in SwiftUI”. Bring this mental model with you as we see how declarative, functional, and immutable programming *across* the stack of our applications leads to code that is easy to reason about, easy to make changes to, and runs faster with less memory than SwiftData. 88 | 89 | Let’s get started! 90 | 91 | [^1]: https://en.wikipedia.org/wiki/History_of_Facebook 92 | [^2]: https://engineering.fb.com/2010/07/21/core-infra/scaling-facebook-to-500-million-users-and-beyond/ 93 | [^3]: https://techcrunch.com/2009/09/15/facebook-crosses-300-million-users-oh-yeah-and-their-cash-flow-just-went-positive/ 94 | [^4]: https://about.fb.com/news/2012/10/one-billion-people-on-facebook/ 95 | [^5]: https://www.youtube.com/watch?v=GW0rj4sNH2w 96 | [^6]: https://martinfowler.com/bliki/CQRS.html 97 | [^7]: https://www.youtube.com/watch?v=nYkdrAPrdcw 98 | [^8]: https://www.reuters.com/article/net-us-facebook-roadshow/facebooks-zuckerberg-says-mobile-first-priority-idUSBRE84A18520120512/ 99 | [^9]: https://engineering.fb.com/2015/03/25/ios/introducing-componentkit-functional-and-declarative-ui-on-ios/ 100 | [^10]: https://engineering.fb.com/2015/09/14/core-infra/relay-declarative-data-for-react-applications/ 101 | [^11]: https://engineering.fb.com/2015/09/14/core-infra/graphql-a-data-query-language/ 102 | [^12]: https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31 103 | [^13]: https://www.youtube.com/watch?v=xsSnOQynTHs 104 | [^14]: https://redux.js.org/understanding/history-and-design/prior-art 105 | [^15]: https://facebookarchive.github.io/flux/ 106 | [^16]: https://en.wikipedia.org/wiki/Swift_(programming_language)#History 107 | [^17]: https://componentkit.org/docs/why-cpp#efficiency 108 | [^18]: https://engineering.fb.com/2016/04/13/ios/building-and-managing-ios-model-objects-with-remodel/ 109 | [^19]: https://www.swift.org/documentation/articles/value-and-reference-types.html 110 | [^20]: https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes#Choose-Structures-by-Default 111 | [^21]: https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Model-View-Controller/Model-View-Controller.html 112 | [^22]: https://developer.apple.com/library/archive/referencelibrary/GettingStarted/RoadMapiOS-Legacy/chapters/StreamlineYourAppswithDesignPatterns/StreamlineYourApps/StreamlineYourApps.html 113 | [^23]: https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html 114 | [^24]: https://developer.apple.com/documentation/uikit/about_app_development_with_uikit 115 | [^25]: https://mjtsai.com/blog/2018/05/01/scuttlebutt-regarding-apples-cross-platform-ui-project/ 116 | [^26]: https://developer.apple.com/videos/play/wwdc2019/103 117 | [^27]: https://developer.apple.com/videos/play/wwdc2019/226 118 | [^28]: https://www.youtube.com/watch?v=4GjXq2Sr55Q 119 | [^29]: https://developer.apple.com/videos/play/wwdc2023/10187 120 | -------------------------------------------------------------------------------- /Chapters/Chapter-03.md: -------------------------------------------------------------------------------- 1 | # CounterData 2 | 3 | Our `ImmutableData` and `ImmutableUI` modules are the “infra” of our project: these are the modules we share across multiple sample application products. These modules make a minimum amount of assumptions and restrictions about what kind of products are built. There’s a lot of power and flexibility with this approach, but the previous chapters might have felt a little abstract without a concrete example of how this is all actually used in production. 4 | 5 | Our first sample application product will be simple; it’s the “Hello World” of Redux. Our application is called Counter. All we do is present a UI to increment and decrement an integer. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | There’s no network connection and no persistent data storage in our filesystem, but this very simple application will help us see the infra actually being used in a product. Once we have an introduction and build our first product, we will see how the `ImmutableData` architecture can scale to more complex products. 14 | 15 | Our Counter application is inspired by the Counter application for Redux.[^1] 16 | 17 | ## CounterState 18 | 19 | Every product we build on the `ImmutableData` architecture will share the same infra modules. Types like `Store` are generic across the State that a product engineer defines. Now that we are product engineering, it’s our job to define what State looks like for our product domain. 20 | 21 | Let’s have a quick review of a concept introduced earlier. We can think of two “buckets” of state that might describe our application at a moment in time. *Local State* — or *Component State* — can be thought of as ephemeral and transient state that reflects details about how our UI is displayed, but not details about the *data* that is driving that UI. We save Local State *locally* in our component tree using `SwiftUI.State`. 22 | 23 | Suppose we build an application for displaying a list of contacts. Our list could be very large and our list should support scrolling. The scroll position of our list component would be an example of what we consider Local State. Our list could also support filtering to search for contacts by name. The substring we currently use to search for contacts we consider Local State. Our list could also support sorting by name ascending or descending. Our sort order we consider Local State. 24 | 25 | *Global State* can be thought of as state that reflects the intrinsic data that drives our application. We save Global State *globally* in our `Store`. 26 | 27 | Suppose our contacts application supports the ability to change the name of a saved contact. We consider the names of our contacts to be Global State. Suppose our contacts application supports the ability to add or delete contacts. We consider the set of all our contacts to be Global State. 28 | 29 | There is no “one right way” to distinguish between what should be thought of as Local State and Global State. For the most part, product engineers have the ability to choose what works best for their product domain. We will see plenty of examples of both types of state before our tutorials are complete. 30 | 31 | An interesting “thought experiment” for trying to separate these two types of state is what we think of as a “Window Test”. Suppose our contacts application supports multiple windows on macOS. If the user opens two different windows, should those windows display the same contacts? Should the contacts displayed by those windows have consistent names? Should deleting a contact from one window delete the contact from the other window? We consider these to be Global State; these contacts should be saved and shared globally across multiple windows. 32 | 33 | Let’s continue with this example. If the user opens two different windows and then scrolls the second window to the last contact in the list, should the first window match the same scroll position? If the user selects the second window and starts to filter for a matching name string, should the first window match to that same name filter? If the user selects the second window and changes the sort order from ascending to descending, should the first window match to that same sort order? We consider these to be Local State; this is transient and ephemeral UI state that is local to one component subgraph. 34 | 35 | For our first sample application product, state is going to be very simple. All we need is one integer which can be incremented and decremented. We’ll see much more complex examples in our next sample application products. 36 | 37 | Select the `CounterData` package and add a new Swift file under `Sources/CounterData`. Name this file `CounterState.swift`. 38 | 39 | Here is our first step: 40 | 41 | ```swift 42 | // CounterState.swift 43 | 44 | public struct CounterState: Sendable { 45 | package var value: Int 46 | 47 | public init(_ value: Int = 0) { 48 | self.value = value 49 | } 50 | } 51 | ``` 52 | 53 | For all products built from the `ImmutableData` architecture, we model State as an immutable value type: typically a `struct`. Any slice of State will also be an immutable value type. Our State types will be complex, but all stored instance properties should map to graphs of value types. 54 | 55 | Our `CounterState` is `public` because we import this type in our next package. Our `value` is `package` because we read this property in our test target. Our `value` is not `public`. To read this property in our next package, we define a Selector function. This is a simple function type; our Selector is trivial — we just return the `value`. A complex product with many subgraphs of components might define many different Selectors. Product Engineers have the ability to define as many Selectors as they might need. 56 | 57 | You might be wondering: what if every product defines only one Selector and returns the entire global state to every component? We built our `ImmutableUI.Selector` to update our component tree with `Observable` when the selected slice of state changes. As an optimization, we build every component to try and select the smallest slice of state it needs to display its data. If every component selects the *entire* global state this can lead to slower performance: state updates that do not affect a component lead to a component recomputing its `body` more times than necessary.[^2] 58 | 59 | Here is our Selector function which returns our `value` to our component tree: 60 | 61 | ```swift 62 | // CounterState.swift 63 | 64 | extension CounterState { 65 | public static func selectValue() -> @Sendable (Self) -> Int { 66 | { state in state.value } 67 | } 68 | } 69 | ``` 70 | 71 | Our `selectValue` function takes no parameters and returns a closure. That closure takes a `CounterState` as a parameter and returns an `Int`. 72 | 73 | The closures returned by our Selectors will always be pure functions: no side effects and no non-deterministic behavior. We also must return synchronously. 74 | 75 | This is all we need to build `CounterState` for our product. This is a very simple product, but think of this as good practice to see this architecture being used before we move on to move complex products. 76 | 77 | ## CounterAction 78 | 79 | As previously discussed, a goal with the `ImmutableData` architecture is to take the experience you have “thinking declaratively” for building graphs of view components and then use that experience to begin thinking declaratively about global state. 80 | 81 | In a SwiftUI application using SwiftData, our view component tree uses imperative logic on mutable model objects when it wants to transform global state. SwiftUI encourages you to think of “the what not the how” for building your view component tree. Let’s then think of “the what not the how” for managing our global state. 82 | 83 | Our SwiftUI counter application is very simple: an “Increment” button, a “Decrement” button, and a component to display the current value. Let’s start with the Increment Button. What happens when a user taps the Increment Button? In a SwiftData application, we could then perform some imperative logic on a mutable model object. Let’s think about how we would make this more declarative. How would a view component declare to a `Store` that a user taps the Increment Button? What would this action be called? Suppose you were to just tell me or another person what this action is for. What is the message you want to dispatch when a user taps the Increment Button? Let’s try to brainstorm some ideas about this. 84 | 85 | What about `IncrementValue`? When the user taps the Increment Button, we dispatch the `IncrementValue` action to a `Store`. Our view component is no longer performing its own imperative logic, but is instead just passing that imperative logic to the `Store` in another form. Let’s keep trying new ideas. Our goal right now is to *think declaratively*. Focus on “the what not the how”. Think of *what* just happened. Try thinking in the past tense. What just happened? 86 | 87 | What about `DidIncrementValue`? We’re thinking in the past tense, but we’re just moving our imperative logic into the past tense. We’re trying to *think declaratively*. We’re not trying to “rephrase” our imperative logic. All we are doing is “publishing the news”. What just happened? Why is this action being dispatched? Don’t try and overthink it — just literally try and think of what we tell our `Store` when the user taps the Increment Button. 88 | 89 | What about `DidTapIncrementButton`? Doesn’t that sound kind of “weird”? Well… it’s not weird; it actually does a great job at telling our `Store` what just happened: the user just tapped the Increment Button. We shouldn’t be overthinking things when we name this action. On the other hand, your instincts might be telling you this action is not “imperative” enough, or that this action somehow will not do a good job at “communicating” how the `Store` should behave. Remember, our goal right now is to *think declaratively*. Our goal is not to communicate *the how*. Our goal is to communicate *the what*. What just happened to cause this action to dispatch to our `Store`? The user just tapped the Increment Button. 90 | 91 | From our time spent teaching engineers the Flux and Redux architectures, exercises like what we just completed help work through one of the biggest obstacles we see. Engineers that completely understand how to think declaratively when it’s time to put their view components on screen seem to keep coming back to imperative thinking when it’s time to notify their `Store` when an important event occurred. 92 | 93 | Our experience is that thinking of these actions as a statement in the past tense is one of the tricks to help keep your mind thinking declaratively. One more important trick is to not try and overthink things. Try and literally name your action what just happened: `DidTapIncrementButton` and `DidTapDecrementButton`. 94 | 95 | Add a new Swift file under `Sources/CounterData`. Name this file `CounterAction.swift`. 96 | 97 | ```swift 98 | // CounterAction.swift 99 | 100 | public enum CounterAction : Sendable { 101 | case didTapIncrementButton 102 | case didTapDecrementButton 103 | } 104 | ``` 105 | 106 | Like our State, we model our Action as an immutable value type — not an object. Modeling our Action as an `enum` helps for building our Reducer functions. You *could* try and model your Action as a `struct`, but we very strongly recommend modeling your Action as an `enum`. If a specific `case` requires some payload or context to be delivered along with it, we can pass this payload as associated values: we will see many examples of this in our more complex sample application products. 107 | 108 | ## CounterReducer 109 | 110 | Let’s review the role of Reducer function types in the `ImmutableData` architecture: 111 | 112 | ```mermaid 113 | flowchart LR 114 | accTitle: Data Flow through Reducers 115 | accDescr: Our reducer maps from a State and an Action to a new State. 116 | oldState[State] --> Reducer 117 | Action --> Reducer 118 | Reducer --> newState[State] 119 | ``` 120 | 121 | Just like Redux, our Reducer function types are pure functions without side effects that map a State and an Action to the next State of our system. In SwiftData, the global state of our system can be mutated at any time by any view component in our graph. In the `ImmutableData` architecture, *any and all* transformations of global state happen *only* because of an Action that is dispatched to our Reducer through our `Store`. 122 | 123 | When we built our Action, our goal was to *think declaratively*. Instead of building Action values that tell our `Store` *how* to behave, we build Action values that tell our `Store` *what* just happened. Now that we are building a Reducer, this *is* the appropriate place to think imperatively. Our goal will be to transform the declarative messages from our component tree to imperative logic that returns a new State. 124 | 125 | Our first sample application product is very simple; this is by design. The concepts and patterns we see in this chapter demonstrate some of the most important opinions and philosophies in the `ImmutableData` architecture. Instead of just writing code, take some time to think through what it is we are building. What opinions are we making? How are those opinions different than what you might have experienced working with SwiftData? 126 | 127 | Add a new Swift file under `Sources/CounterData`. Name this file `CounterReducer.swift`. 128 | 129 | ```swift 130 | // CounterReducer.swift 131 | 132 | public enum CounterReducer { 133 | @Sendable public static func reduce( 134 | state: CounterState, 135 | action: CounterAction 136 | ) -> CounterState { 137 | switch action { 138 | case .didTapIncrementButton: 139 | var state = state 140 | state.value += 1 141 | return state 142 | case .didTapDecrementButton: 143 | var state = state 144 | state.value -= 1 145 | return state 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | Our `reduce` function takes two parameters: a `CounterState` and an `CounterAction`. We return a new `CounterState`. This pure function is synchronous, deterministic, and free of side effects. 152 | 153 | Modeling our `CounterAction` as an enum gives us the quick and easy ability to `switch` over potential Action values. The total set of all Action values can be very large in complex products; we will discuss some strategies in our future chapters to keep these Reducers approachable and organized. 154 | 155 | Once we `switch` over our Action value, we can then define our own imperative logic on every case. This logic transforms the previous state of our system to the next state of our system. In this example, every `case` maps to one transformation; in practice, is it totally legit for an Action value to “fall through” and result in no transformation. In that situation, we just return the original state. We will see examples of this technique in our next sample application product. 156 | 157 | Our `reduce` function is modeled as a `static` function on a `enum` type. Another option would be to model our `reduce` function as a “standalone” or “free” function outside of any type. We don’t have a very strong opinion here. Our `reduce` function is stateless by design; it could be modeled as a standalone function. Our convention will be to define our `reduce` functions on a type, but you can choose a different approach if this is more consistent with style throughout your own projects. 158 | 159 | --- 160 | 161 | Here is our `CounterData` package, including the tests available on our `chapter-03` branch: 162 | 163 | ```text 164 | CounterData 165 | ├── Sources 166 | │   └── CounterData 167 | │   ├── CounterAction.swift 168 | │   ├── CounterReducer.swift 169 | │   └── CounterState.swift 170 | └── Tests 171 | └── CounterDataTests 172 | ├── CounterReducerTests.swift 173 | └── CounterStateTests.swift 174 | ``` 175 | 176 | These three types represent the data model layer of our first sample application product. We are ready to build the view component tree in our next steps. Let’s quickly review some important concepts we discussed that will also apply to all products we build on this architecture: 177 | 178 | * We model our State as an immutable value type. Our recommendation is to use a `struct`. 179 | * We model our Action as an immutable value type. Our recommendation is to use an `enum`. 180 | * We model our Reducer as a stateless and synchronous function with no side effects and no non-deterministic behavior. 181 | 182 | Our previous chapters built the infra modules that are shared across sample application products. This chapter was our first experience building code tied to the specific domain of just one product, but we discussed some important opinions and philosophies that we will use throughout our tutorials. 183 | 184 | [^1]: https://redux.js.org/tutorials/quick-start 185 | [^2]: https://swiftui-lab.com/random-lessons/#data-5 186 | -------------------------------------------------------------------------------- /Chapters/Chapter-04.md: -------------------------------------------------------------------------------- 1 | # CounterUI 2 | 3 | Our Counter sample application product is almost complete. Let’s turn our attention to the SwiftUI view component tree. Let’s review the three components we are going to display for the user: an Increment button, a Decrement button, and a component to display the current value. To display the current value, we will use `ImmutableUI.Selector`. To mutate the current value, we will dispatch actions using `ImmutableUI.Dispatcher`. To pass a `Store` through our component tree, we will use `ImmutableUI.Provider`. 4 | 5 | ## StoreKey 6 | 7 | The components we built in the `ImmutableUI` module use `SwiftUI.Environment` to read a `Store`. What we didn’t define in `ImmutableUI` is what key path we use to set this `Store` on `SwiftUI.Environment`. This decision we left to product engineers. Since we are now product engineers, let’s start by defining a key that will be used across our application to save this `Store`. 8 | 9 | Select the `CounterUI` package and add a new Swift file under `Sources/CounterUI`. Name this file `StoreKey.swift`. 10 | 11 | ```swift 12 | // StoreKey.swift 13 | 14 | import CounterData 15 | import ImmutableData 16 | import ImmutableUI 17 | import SwiftUI 18 | 19 | @MainActor fileprivate struct StoreKey : @preconcurrency EnvironmentKey { 20 | static let defaultValue = ImmutableData.Store( 21 | initialState: CounterState(), 22 | reducer: CounterReducer.reduce 23 | ) 24 | } 25 | ``` 26 | 27 | Our `StoreKey` adopts `EnvironmentKey`. Our `defaultValue` creates a new `Store` instance with `CounterState` as the `Store.State` and `CounterAction` as the `Store.Action`. 28 | 29 | We are required to provide a `defaultValue` for `EnvironmentKey`,[^1] but our application will provide its own instance through `ImmutableUI.Provider`. The instance we provide through `ImmutableUI.Provider` will then be the `Store` available to `ImmutableUI.Selector` and `ImmutableUI.Dispatcher`. If you wish to enforce that the `defaultValue` is not appropriate to use through `ImmutableUI.Selector` and `ImmutableUI.Dispatcher`, you could also choose to pass a custom `reduce` function that crashes with a `fatalError` to indicate a programmer error. 30 | 31 | To compile from Swift 6.0 and Strict Concurrency Checking, we adopt `preconcurrency` from [SE-0423][^2]. 32 | 33 | Our next step is an extension on `EnvironmentValues`: 34 | 35 | ```swift 36 | // StoreKey.swift 37 | 38 | extension EnvironmentValues { 39 | fileprivate var store: ImmutableData.Store { 40 | get { 41 | self[StoreKey.self] 42 | } 43 | set { 44 | self[StoreKey.self] = newValue 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | When building from Xcode 16.0, we have the option to save ourselves some time with the `Entry` macro. As of this writing, this seems to lead to our `defaultValue` being created on demand many times over our app lifecycle.[^3] For now, we build these the old-fashioned way to save our `defaultValue` as a stored type property: it’s created just once. 51 | 52 | We can update our types from our `ImmutableUI` infra to take advantage of `StoreKey`. Our `ImmutableUI` types take an `EnvironmentValues` key path to a `Store` as a parameter. As you can imagine, it would be tedious to have to pass the same exact key path in every place we need to call one of these types. Since our product will use the same key path across our application life cycle, we can update the types from `ImmutableUI` to use this key path without us having to pass it as a parameter. Let’s begin with our `ImmutableUI.Provider`: 53 | 54 | ```swift 55 | // StoreKey.swift 56 | 57 | extension ImmutableUI.Provider { 58 | public init( 59 | _ store: Store, 60 | @ViewBuilder content: () -> Content 61 | ) where Store == ImmutableData.Store { 62 | self.init( 63 | \.store, 64 | store, 65 | content: content 66 | ) 67 | } 68 | } 69 | ``` 70 | 71 | This extension on `ImmutableUI.Provider` adds a new initializer. The original initializer accepts an `EnvironmentValues` key path as a parameter. Since we defined our `StoreKey` on the expectation that our application uses this key path everywhere across the component tree, we can define this new initializer that uses our `Store` key path every time. This also allows us to keep our `StoreKey` defined as `fileprivate` while still defining a `public` initializer on `ImmutableUI.Provider` that uses our `Store` key path. 72 | 73 | Let’s continue with a new initializer for `ImmutableUI.Dispatcher`: 74 | 75 | ```swift 76 | // StoreKey.swift 77 | 78 | extension ImmutableUI.Dispatcher { 79 | public init() where Store == ImmutableData.Store { 80 | self.init(\.store) 81 | } 82 | } 83 | ``` 84 | 85 | Let’s finish with our `ImmutableUI.Selector` initializers: 86 | 87 | ```swift 88 | // StoreKey.swift 89 | 90 | extension ImmutableUI.Selector { 91 | public init( 92 | id: some Hashable, 93 | label: String? = nil, 94 | filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, 95 | dependencySelector: repeat DependencySelector, 96 | outputSelector: OutputSelector 97 | ) where Store == ImmutableData.Store { 98 | self.init( 99 | \.store, 100 | id: id, 101 | label: label, 102 | filter: isIncluded, 103 | dependencySelector: repeat each dependencySelector, 104 | outputSelector: outputSelector 105 | ) 106 | } 107 | } 108 | 109 | extension ImmutableUI.Selector { 110 | public init( 111 | label: String? = nil, 112 | filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, 113 | dependencySelector: repeat DependencySelector, 114 | outputSelector: OutputSelector 115 | ) where Store == ImmutableData.Store { 116 | self.init( 117 | \.store, 118 | label: label, 119 | filter: isIncluded, 120 | dependencySelector: repeat each dependencySelector, 121 | outputSelector: outputSelector 122 | ) 123 | } 124 | } 125 | ``` 126 | 127 | Each sample application product will be required to provide its own key path to access a `Store` instance from `EnvironmentValues`. While we are not required to build these new initializers, this is the convention we follow for our sample application products through this tutorial. You are welcome to follow this convention for your own sample application products. 128 | 129 | ## Dispatch 130 | 131 | Our component tree will need to dispatch an Action when the user taps a button. We will use the `ImmutableUI.Dispatcher` type to dispatch this action. When we built the `ImmutableUI.Dispatcher`, we returned an opaque `ImmutableData.Dispatcher` as a `wrappedValue`. When we built the `ImmutableData.Dispatcher` protocol, we included a function for dispatching “thunk” closures. We will see in our next sample application product how we use these thunks to dispatch async operations. 132 | 133 | While we *could* use `ImmutableUI.Dispatcher` to dispatch thunk operations directly from our component tree, we will see that our recommended approach for that will be to use Listeners in our data model layer. 134 | 135 | Let’s build a new property wrapper just for our product. This property wrapper will make use of `ImmutableUI.Dispatcher`, but will only give product engineers the ability to dispatch Action values. 136 | 137 | Add a new Swift file under `Sources/CounterUI`. Name this file `Dispatch.swift`. 138 | 139 | ```swift 140 | // Dispatch.swift 141 | 142 | import CounterData 143 | import ImmutableData 144 | import ImmutableUI 145 | import SwiftUI 146 | 147 | @MainActor @propertyWrapper struct Dispatch : DynamicProperty { 148 | @ImmutableUI.Dispatcher() private var dispatcher 149 | 150 | init() { 151 | 152 | } 153 | 154 | var wrappedValue: (CounterAction) throws -> () { 155 | self.dispatcher.dispatch 156 | } 157 | } 158 | ``` 159 | 160 | For our `wrappedValue`, we return the `dispatch` function from our `dispatcher`. We do not give product engineers the option to dispatch a thunk operation from components with this type. 161 | 162 | Product engineers are not required to build their own `Dispatch` property wrapper, but we do recommend most product engineers follow this pattern. 163 | 164 | ## Select 165 | 166 | Our component tree will need to display the current value. We will use the `ImmutableUI.Selector` type to select this value from our State. The `ImmutableUI.Selector` offers a lot of power and flexibility to product engineers to customize the behavior for their product, but all this flexibility might not always be necessary or desirable. A “simpler” API would reduce the friction on product engineers. Let’s see an example of how we can customize the behavior of `ImmutableUI.Selector` to help improve the developer experience for our product engineers building the Counter application. 167 | 168 | The `ImmutableUI.DependencySelector` and `ImmutableUI.OutputSelector` give product engineers the ability to define the slices of State they need to be their dependencies and output. These types also offer product engineers the ability to define their own `didChange` closures for indicating two values have changed. When two `Dependency` values have changed, we compute a new `Output` instance. When two `Output` values have changed, we use `Observable` to compute a new `body` in our component. 169 | 170 | The ability for product engineers to have control over what logic is used to determine when two `Dependency` or `Output` values have changed is very powerful, but can introduce “ceremony” throughout our product. What our product engineers usually want is the ability to define some “default” behavior to save time from writing repetitive boilerplate code. Let’s see how we can build this for our product. 171 | 172 | Add a new Swift file under `Sources/CounterUI`. Name this file `Select.swift`. 173 | 174 | ```swift 175 | // Select.swift 176 | 177 | import CounterData 178 | import ImmutableData 179 | import ImmutableUI 180 | import SwiftUI 181 | 182 | extension ImmutableUI.DependencySelector { 183 | init(select: @escaping @Sendable (State) -> Dependency) where Dependency : Equatable { 184 | self.init(select: select, didChange: { $0 != $1 }) 185 | } 186 | } 187 | 188 | extension ImmutableUI.OutputSelector { 189 | init(select: @escaping @Sendable (State) -> Output) where Output : Equatable { 190 | self.init(select: select, didChange: { $0 != $1 }) 191 | } 192 | } 193 | ``` 194 | 195 | Our first step is to add some default behavior to `ImmutableUI.DependencySelector` and `ImmutableUI.OutputSelector`. These new initializers are available when `Dependency` and `Output` adopt `Equatable`. For this product, we decided that a reasonable default behavior is that we want to use value equality to determine whether or not two values have changed. Rather than require product engineers to pass this value equality operator to *every* `ImmutableUI.DependencySelector` and `ImmutableUI.OutputSelector`, we build two new initializers that pass those value equality operators for us. 196 | 197 | Now that we have these new initializers, we can also simplify the initializers on `ImmutableUI.Selector`: 198 | 199 | ```swift 200 | // Select.swift 201 | 202 | extension ImmutableUI.Selector { 203 | init( 204 | id: some Hashable, 205 | label: String? = nil, 206 | filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, 207 | dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, 208 | outputSelector: @escaping @Sendable (Store.State) -> Output 209 | ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { 210 | self.init( 211 | id: id, 212 | label: label, 213 | filter: isIncluded, 214 | dependencySelector: repeat DependencySelector(select: each dependencySelector), 215 | outputSelector: OutputSelector(select: outputSelector) 216 | ) 217 | } 218 | } 219 | 220 | extension ImmutableUI.Selector { 221 | init( 222 | label: String? = nil, 223 | filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, 224 | dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, 225 | outputSelector: @escaping @Sendable (Store.State) -> Output 226 | ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { 227 | self.init( 228 | label: label, 229 | filter: isIncluded, 230 | dependencySelector: repeat DependencySelector(select: each dependencySelector), 231 | outputSelector: OutputSelector(select: outputSelector) 232 | ) 233 | } 234 | } 235 | ``` 236 | 237 | Now, we don’t have to specify a value equality operator when creating a `ImmutableUI.Selector` instance; our product defines its own default behavior which is appropriate for the domain of this product. 238 | 239 | Not every product engineer is going to want to use these defaults; some product engineers might need the flexibility and control of the original initializers. We expect that value equality would be a reasonable default behavior for most product engineers and this is the convention we follow in our sample application products. You are welcome to follow this convention for your own sample application products. 240 | 241 | In our previous chapter, we defined the `selectValue` function to select and return our `value` from our `CounterState`. This will be the `outputSelector` we pass when creating `ImmutableUI.Selector` for displaying the value in our component tree. In complex products, we might need to display the same slice of State across multiple view component subgraphs. To help make things easier for product engineers and reduce the amount of code that needs to be duplicated, we will define custom Selector types for our products. Each Selector type knows how to select one specific slice of State and return that slice to a view component. 242 | 243 | Here is our custom Selector: 244 | 245 | ```swift 246 | // Select.swift 247 | 248 | @MainActor @propertyWrapper struct SelectValue : DynamicProperty { 249 | @ImmutableUI.Selector(outputSelector: CounterState.selectValue()) var wrappedValue 250 | 251 | init() { 252 | 253 | } 254 | } 255 | ``` 256 | 257 | Our sample application product will only display `value` in one place; but this is good practice for us before we move on to more complex products. We will follow this convention throughout our sample application products. We encourage you to follow this convention in your own products. 258 | 259 | Our use of custom Selector types serves a similar role as custom hooks in React.[^4] 260 | 261 | ## Content 262 | 263 | You might expect that building our view component tree would be a lot of work. The truth is, most of the “heavy lifting” has already been completed; this was by design. For the most part, learning the `ImmutableData` architecture does not mean learning SwiftUI all over again. Most of our work building component trees will look and feel very familiar to what you already know; this was also by design. 264 | 265 | The biggest philosophical difference you must embrace is transforming user events to Action values. This is what we mean by *thinking declaratively*. Instead of performing an imperative mutation on global state when a button is tapped, we dispatch an Action to our `Store`. 266 | 267 | Let’s start by building our component. Add a new Swift file under `Sources/CounterUI`. Name this file `Content.swift`. Here is the declaration: 268 | 269 | ```swift 270 | // Content.swift 271 | 272 | import CounterData 273 | import ImmutableData 274 | import ImmutableUI 275 | import SwiftUI 276 | 277 | @MainActor public struct Content { 278 | @SelectValue private var value 279 | 280 | @Dispatch private var dispatch 281 | 282 | public init() { 283 | 284 | } 285 | } 286 | ``` 287 | 288 | Our `Content` uses the property wrappers we built earlier in this chapter. The `Dispatch` wrapper returns our `dispatch` function. The `SelectValue` wrapper returns our `value`. 289 | 290 | Our next step is two helper functions for dispatching Action values: 291 | 292 | ```swift 293 | // Content.swift 294 | 295 | extension Content { 296 | private func didTapIncrementButton() { 297 | do { 298 | try self.dispatch(.didTapIncrementButton) 299 | } catch { 300 | print(error) 301 | } 302 | } 303 | } 304 | 305 | extension Content { 306 | private func didTapDecrementButton() { 307 | do { 308 | try self.dispatch(.didTapDecrementButton) 309 | } catch { 310 | print(error) 311 | } 312 | } 313 | } 314 | ``` 315 | 316 | Our `dispatch` function can throw an error. For this product, we `print` that `error` to our console. A discussion on error handling for SwiftUI is orthogonal to our goal of teaching the `ImmutableData` architecture. In your own products, you might choose to display an alert to the user. For our tutorial, we `print` to console in the interest of time to keep things concentrated on `ImmutableData` as much as possible. If you did choose to display an alert to the user, the state to manage that alert could be saved as “component” state using `SwiftUI.State`; you don’t need to rethink how your global state is built. 317 | 318 | Let’s turn our attention to the component tree: 319 | 320 | ```swift 321 | // Content.swift 322 | 323 | extension Content : View { 324 | public var body: some View { 325 | VStack { 326 | Button("Increment") { 327 | self.didTapIncrementButton() 328 | } 329 | Text("Value: \(self.value)") 330 | Button("Decrement") { 331 | self.didTapDecrementButton() 332 | } 333 | } 334 | .frame( 335 | minWidth: 256, 336 | minHeight: 256 337 | ) 338 | } 339 | } 340 | 341 | #Preview { 342 | Content() 343 | } 344 | ``` 345 | 346 | In macOS 15.0, there seems to be a known issue in `SwiftUI.Stepper` that is causing unexpected behavior when using `Observable`.[^5] We can work around this with a custom component. 347 | 348 | You can now see this component tree live from Xcode in `Preview`. This is actually the first time we’ve seen our `ImmutableData` in action from a live demo. Pretty cool! Tapping the Increment Button dispatches the `didTapIncrementButton` value to our `Store`. Tapping the Decrement Button performs the inverse operation and dispatches the `didTapDecrementButton` value to our `Store`. Our `CounterReducer` transforms our `CounterState` to increment our `value`. Our component tree is updated when `value` changes because we built our `ImmutableUI.Selector` infra on `Observable`. 349 | 350 | The `Content` component from `Preview` is using the `StoreKey.defaultValue` we defined earlier in this chapter. For your products, you might choose to keep `StoreKey.defaultValue` as a legit option that product engineers can choose to use from `Preview`. In the products through this tutorial, we will always use `Provider` to set one `Store` instance on `Environment` at the root level of the component tree. 351 | 352 | The `Preview` macro will be very helpful to us as we build our component trees. Let’s see an example of how we can have more control over the `Store` instance used in `Preview`. 353 | 354 | As we previously discussed, you might choose to pass a Reducer function to your `StoreKey.defaultValue` instance that crashes with a `fatalError` to indicate programmer error: product engineers should always use the `Store` instance that is passed through `Provider`. Let’s see an example of a Preview that passes a `Store` through `Provider`: 355 | 356 | ```swift 357 | // Content.swift 358 | 359 | #Preview { 360 | @Previewable @State var store = ImmutableData.Store( 361 | initialState: CounterState(), 362 | reducer: CounterReducer.reduce 363 | ) 364 | 365 | Provider(store) { 366 | Content() 367 | } 368 | } 369 | ``` 370 | 371 | Our `CounterReducer.reduce` function does not throw errors, but we still perform a `do-catch` statement in `Content` because the `ImmutableData.Dispatcher` protocol says the `dispatch` function may choose to throw. Our `Content` component logs these errors to console, but we can’t see these errors in our app; they don’t exist. 372 | 373 | Let’s add a custom Preview just for tracking our error logging: 374 | 375 | ```swift 376 | // Content.swift 377 | 378 | fileprivate struct CounterError : Swift.Error { 379 | let state: CounterState 380 | let action: CounterAction 381 | } 382 | 383 | #Preview { 384 | @Previewable @State var store = ImmutableData.Store( 385 | initialState: CounterState(), 386 | reducer: { (state: CounterState, action: CounterAction) -> (CounterState) in 387 | throw CounterError( 388 | state: state, 389 | action: action 390 | ) 391 | } 392 | ) 393 | 394 | Provider(store) { 395 | Content() 396 | } 397 | } 398 | ``` 399 | 400 | Instead of creating a `Store` instance with `CounterReducer.reduce`, we define a custom Reducer function that *does* throw errors. When we run this Preview from Xcode, we can confirm that errors are printed to console when our Button components are tapped. 401 | 402 | Passing custom reducers to a Preview can be a legit strategy to improve testability, but our advice is to try and save this technique for special circumstances. For this example, we build a custom Reducer because our production Reducer never throws. We make sure to include a Preview with our production Reducer; we want product engineers to have the ability to test against the same Reducer our users will see in the final product. 403 | 404 | --- 405 | 406 | Here is our `CounterUI` package: 407 | 408 | ```text 409 | CounterUI 410 | └── Sources 411 | └── CounterUI 412 | ├── Content.swift 413 | ├── Dispatch.swift 414 | ├── Select.swift 415 | └── StoreKey.swift 416 | ``` 417 | 418 | Unlike our previous chapters, we don’t have a very strong opinion on unit testing your view component tree built from `ImmutableData`. We consider unit testing SwiftUI to be orthogonal to our goal of teaching the `ImmutableData` architecture. For this tutorial, we prefer “integration-style” testing using `Preview`. If you are interested in learning more about unit testing for SwiftUI, we recommend following Jon Reid and Quality Coding to learn what might be possible for your products.[^6] 419 | 420 | [^1]: https://developer.apple.com/documentation/swiftui/environmentkey/ 421 | [^2]: https://github.com/apple/swift-evolution/blob/main/proposals/0423-dynamic-actor-isolation.md 422 | [^3]: https://developer.apple.com/forums/thread/770298 423 | [^4]: https://react.dev/learn/reusing-logic-with-custom-hooks 424 | [^5]: https://developer.apple.com/forums/thread/763442 425 | [^6]: https://qualitycoding.org 426 | -------------------------------------------------------------------------------- /Chapters/Chapter-05.md: -------------------------------------------------------------------------------- 1 | # Counter.app 2 | 3 | The final step to complete our Counter application is to build a `SwiftUI.App` entry point. 4 | 5 | Select `Counter.xcodeproj` and open `CounterApp.swift`. We can delete the original “Hello World” template. Let’s begin by defining our `CounterApp` type: 6 | 7 | ```swift 8 | // CounterApp.swift 9 | 10 | import CounterData 11 | import CounterUI 12 | import ImmutableData 13 | import ImmutableUI 14 | import SwiftUI 15 | 16 | @main @MainActor struct CounterApp { 17 | @State private var store = Store( 18 | initialState: CounterState(), 19 | reducer: CounterReducer.reduce 20 | ) 21 | } 22 | ``` 23 | 24 | In our previous chapter, we saw that we are required to provide a `defaultValue` for `StoreKey`. While you *could* use this `defaultValue` as the global state for your application, we strongly recommend creating a `Store` in your `App` entry point. In our next sample application products, we also see how this is where we configure our Listeners to manage complex asynchronous operations on behalf of our `Store`. 25 | 26 | Now that we have a `Store` instance, let’s add a `body` to complete our application: 27 | 28 | ```swift 29 | // CounterApp.swift 30 | 31 | extension CounterApp : App { 32 | var body: some Scene { 33 | WindowGroup { 34 | Provider(self.store) { 35 | Content() 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | We deliver our `Store` instance through our component tree using the `Provider` type. We don’t have to provide an explicit key path because this is using the extension initializer we defined in our previous chapter. 43 | 44 | That should be all we need for now. Go ahead and build and run (`⌘ R`) and watch our Counter application run on your computer. This window should function like what we saw in the Previews we built in our previous chapter: Our Increment Button and Decrement Button update our Value component. What we didn’t see from `Preview` was multiple windows. Go ahead and create a new window (`⌘ N`) to see them both working together. Change the value in the first window and watch it update in the second window. 45 | 46 | As previously discussed, we focus our attention in this tutorial on building macOS applications. This saves us some time and keeps us moving forward, but the `ImmutableData` infra is meant to be multiplatform: any platform where SwiftUI is available. Nothing about `CounterApp` really *needs* macOS; we could deploy this app to iOS, iPadOS, tvOS, visionOS, or watchOS. Optimizing SwiftUI applications to display on multiple platforms is an important topic, but is mostly a discussion about *presentation* of data; this is orthogonal to our main goal: teaching the `ImmutableData` architecture. We continue to focus on macOS for this tutorial, but we encourage you to deploy `ImmutableData` to all platforms supported by your product. 47 | 48 | This sample application is very simple; all of our state is *global state*: just an integer value. In our next sample application products, we will see examples of much more complex global states along with examples of *local state* which we save in our view component tree directly. 49 | 50 | Finally, we want to discuss some very important topics for front-end engineering: internationalization, localization, and accessibility. Internationalization and localization is the process of presenting data in formats and languages most appropriate for users.[^1] Accessibility is the process of presenting data in a way that everyone can understand your product, including people with disabilities that might use assistive technologies.[^2] Building applications for *all* users is an important goal, but is mostly a discussion about *presentation* of data. This can be learned orthogonally to the `ImmutableData` architecture. We encourage you to explore the resources available from Apple for learning more about these best practices. 51 | 52 | [^1]: https://developer.apple.com/localization/ 53 | [^2]: https://developer.apple.com/accessibility/ 54 | -------------------------------------------------------------------------------- /Chapters/Chapter-07.md: -------------------------------------------------------------------------------- 1 | # AnimalsDataClient 2 | 3 | When building products with dependencies on complex services like SwiftData, it can be very handy to be able to run things “end-to-end” outside our UI. We can think of this as an “integration test”; this is in addition to — not a replacement for — conventional unit tests. 4 | 5 | Before we build our component tree and put these data models on-screen, let’s build a simple command-line executable against our `LocalStore`. Our `AnimalsDataClient` executable will build against our production `LocalStore` class and persist its data on our filesystem. This means we can run our `AnimalsDataClient` executable, mutate our data, and see that new data the next time we run. 6 | 7 | To run against SwiftData from a command-line executable, we need a little work to configure our module.[^1] The Workspace from the [`ImmutableData-Samples`](https://github.com/Swift-ImmutableData/ImmutableData-Samples) repo already includes this configuration to save us some time. All we have to do is select the `AnimalsData` package and open `Sources/AnimalsDataClient/main.swift`. 8 | 9 | Here is all we need to begin operating on a `LocalStore`: 10 | 11 | ```swift 12 | // main.swift 13 | 14 | import AnimalsData 15 | import Foundation 16 | 17 | func makeLocalStore() throws -> LocalStore { 18 | if let url = Process().currentDirectoryURL?.appending( 19 | component: "default.store", 20 | directoryHint: .notDirectory 21 | ) { 22 | return try LocalStore(url: url) 23 | } 24 | return try LocalStore() 25 | } 26 | 27 | func main() async throws { 28 | let store = try makeLocalStore() 29 | 30 | let animals = try await store.fetchAnimalsQuery() 31 | print(animals) 32 | 33 | let categories = try await store.fetchCategoriesQuery() 34 | print(categories) 35 | } 36 | 37 | try await main() 38 | ``` 39 | 40 | Our `main` function builds a `LocalStore` instance. We can then read from our `LocalStore` and perform mutations. 41 | 42 | Try it for yourself: think of this executable as a “playground”. Try adding mutations. Run your executable again and confirm the mutations were persisted. 43 | 44 | [^1]: https://forums.developer.apple.com/forums/thread/734540 45 | -------------------------------------------------------------------------------- /Chapters/Chapter-09.md: -------------------------------------------------------------------------------- 1 | # Animals.app 2 | 3 | Our `AnimalsData` and `AnimalsUI` modules are complete. We just need an `App` to tie everything together. 4 | 5 | Select `Animals.xcodeproj` and open `AnimalsApp.swift`. We can delete the original “Hello World” template. Let’s begin by defining our `AnimalsApp` type: 6 | 7 | ```swift 8 | // AnimalsApp.swift 9 | 10 | import AnimalsData 11 | import AnimalsUI 12 | import ImmutableData 13 | import ImmutableUI 14 | import SwiftUI 15 | 16 | @main @MainActor struct AnimalsApp { 17 | @State private var store = Store( 18 | initialState: AnimalsState(), 19 | reducer: AnimalsReducer.reduce 20 | ) 21 | @State private var listener = Listener(store: Self.makeLocalStore()) 22 | 23 | init() { 24 | self.listener.listen(to: self.store) 25 | } 26 | } 27 | ``` 28 | 29 | We construct our `AnimalsApp` with a `Store` and a `Listener`. Our `Store` is constructed with `AnimalsState` and `AnimalsReducer`. Our `Listener` will be constructed with a `LocalStore` as its `PersistentStore`. We pass our `Store` to our `Listener`; when action values are dispatched to our `Store` and our `AnimalsReducer` returns, this `Listener` will perform asynchronous side effects. 30 | 31 | Here is where we construct our `LocalStore`: 32 | 33 | ```swift 34 | // AnimalsApp.swift 35 | 36 | extension AnimalsApp { 37 | private static func makeLocalStore() -> LocalStore { 38 | do { 39 | return try LocalStore() 40 | } catch { 41 | fatalError("\(error)") 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | If we fail to construct a `LocalStore`, we crash our sample product. A full discussion about how and why SwiftData might fail to initialize is outside the scope of this tutorial. You can explore the documentation and determine for yourself if your own products should perform any work here to “fail gracefully” and inform the user about what just happened. 48 | 49 | Our final step is `body`: 50 | 51 | ```swift 52 | // AnimalsApp.swift 53 | 54 | extension AnimalsApp: App { 55 | var body: some Scene { 56 | WindowGroup { 57 | Provider(self.store) { 58 | Content() 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | We can now build and run (`⌘ R`). Our application is now a working clone of the original Animals sample application from Apple. We preserved the core functionality: `Animal` values can be created, edited, and deleted. We also save our global state on our filesystem in a persistent database. You can also create a new window (`⌘ N`) and watch as edits from one window are reflected in the other. 66 | 67 | Both applications are built from SwiftUI, and both application leverage SwiftData to manage their persistent database. The big difference here is that the original sample application built SwiftUI components that respond to user events with imperative logic to mutate SwiftData model objects. Our new sample application built SwiftUI components that respond to user events with declarative logic; the imperative logic to mutate SwiftData model objects is abstracted out of our component tree. Instead of passing mutable model objects to our component tree — and living with the unpredictable nature of shared mutable state — we pass immutable model values — our component tree can’t mutate shared state unpredictably. 68 | 69 | Let’s experiment with some of the tools we built for improved debugging. Inspired by Antoine van der Lee, we’re going to leverage launch arguments from Xcode to enable some of the `print` statements we built earlier.[^1] Let’s begin with our `AnimalsUI` module. We added a `debugPrint` function that we call when components construct a `body` property. Let’s add a launch argument to our scheme to see how this looks: 70 | 71 | ```text 72 | -com.northbronson.AnimalsUI.Debug 1 73 | ``` 74 | 75 | When we enable this argument and run our application, we now see logging from SwiftUI when our `body` properties are built: 76 | 77 | ```text 78 | Content: @self, @identity, _selectedCategoryId, _selectedAnimalId changed. 79 | CategoryList: @self, @identity, _selectedCategoryId changed. 80 | CategoryList.Container: @self, @identity, _categories, @16, _status, @184, _selectedCategoryId, _dispatch changed. 81 | CategoryList.Presenter: @self, @identity, _isAlertPresented, _selectedCategoryId changed. 82 | AnimalList.Container: @self, @identity, _animals, @16, _category, @184, _status, @320, _selectedAnimalId, _dispatch changed. 83 | AnimalList.Presenter: @self, @identity, _isSheetPresented, _selectedAnimalId changed. 84 | AnimalDetail: @self changed. 85 | AnimalDetail.Container: @self, @identity, _animal, @16, _category, @152, _status, @288, _dispatch changed. 86 | AnimalDetail.Presenter: @self, @identity, _isAlertPresented, _isSheetPresented changed. 87 | CategoryList.Container: \Storage>.output changed. 88 | CategoryList.Presenter: @self changed. 89 | CategoryList.Container: \Storage>.output changed. 90 | CategoryList.Presenter: @self changed. 91 | ``` 92 | 93 | While we perform user events and transform our global state, we can watch as our component tree is rebuilt. To optimize for performance, we can choose to reduce the amount of unnecessary time spent computing `body` properties. Our `ImmutableData` architecture helps us here: our component tree is built against Selectors that return data scoped to the needs of our component. Our Selectors use Filters and Dependencies to reduce the amount of times we then return new data to our component. 94 | 95 | If we select the `Fish` category and add a new `Animal` value to the `Mammal` category, we *do not* see our `AnimalList` component compute its `body` again. If we select the `Fish` category and add a new `Animal` values to the `Fish` category, we *do* see our `AnimalList` component compute its `body` again. 96 | 97 | Let’s enable the logging we built in `AnimalsData`. Here is our new launch argument: 98 | 99 | ```text 100 | -com.northbronson.AnimalsData.Debug 1 101 | ``` 102 | 103 | When we enable this argument and run our application, we now see logging from `AnimalsData`: 104 | 105 | ```text 106 | [AnimalsData][Listener] Old State: AnimalsState(categories: AnimalsData.AnimalsState.Categories(data: [:], status: nil), animals: AnimalsData.AnimalsState.Animals(data: [:], status: nil, queue: [:])) 107 | [AnimalsData][Listener] Action: ui(AnimalsData.AnimalsAction.UI.categoryList(AnimalsData.AnimalsAction.UI.CategoryList.onAppear)) 108 | [AnimalsData][Listener] New State: AnimalsState(categories: AnimalsData.AnimalsState.Categories(data: [:], status: Optional(AnimalsData.Status.waiting)), animals: AnimalsData.AnimalsState.Animals(data: [:], status: nil, queue: [:])) 109 | [AnimalsData][Listener] Old State: AnimalsState(categories: AnimalsData.AnimalsState.Categories(data: [:], status: Optional(AnimalsData.Status.waiting)), animals: AnimalsData.AnimalsState.Animals(data: [:], status: nil, queue: [:])) 110 | [AnimalsData][Listener] Action: data(AnimalsData.AnimalsAction.Data.persistentSession(AnimalsData.AnimalsAction.Data.PersistentSession.didFetchCategories(result: AnimalsData.AnimalsAction.Data.PersistentSession.FetchCategoriesResult.success(categories: [AnimalsData.Category(categoryId: "Invertebrate", name: "Invertebrate"), AnimalsData.Category(categoryId: "Mammal", name: "Mammal"), AnimalsData.Category(categoryId: "Reptile", name: "Reptile"), AnimalsData.Category(categoryId: "Fish", name: "Fish"), AnimalsData.Category(categoryId: "Bird", name: "Bird"), AnimalsData.Category(categoryId: "Amphibian", name: "Amphibian")])))) 111 | [AnimalsData][Listener] New State: AnimalsState(categories: AnimalsData.AnimalsState.Categories(data: ["Bird": AnimalsData.Category(categoryId: "Bird", name: "Bird"), "Fish": AnimalsData.Category(categoryId: "Fish", name: "Fish"), "Reptile": AnimalsData.Category(categoryId: "Reptile", name: "Reptile"), "Invertebrate": AnimalsData.Category(categoryId: "Invertebrate", name: "Invertebrate"), "Mammal": AnimalsData.Category(categoryId: "Mammal", name: "Mammal"), "Amphibian": AnimalsData.Category(categoryId: "Amphibian", name: "Amphibian")], status: Optional(AnimalsData.Status.success)), animals: AnimalsData.AnimalsState.Animals(data: [:], status: nil, queue: [:])) 112 | ``` 113 | 114 | For every action dispatched to our `Store`, our `Listener` instance is now logging the global state, the action value, and the state returned from our Reducer. This can log *a lot* of data when application state grows large and complex, but it can be a very useful tool for investigating unexpected behaviors. 115 | 116 | Let’s enable the logging we built in `ImmutableUI`. Here is our new launch argument: 117 | 118 | ```text 119 | -com.northbronson.ImmutableUI.Debug 1 120 | ``` 121 | 122 | When we enable this argument and run our application, we now see logging from `Listener`: 123 | 124 | ```text 125 | [ImmutableUI][AsyncListener]: 0x0000600000514870 Update: SelectCategoriesValues 126 | [ImmutableUI][AsyncListener]: 0x0000600000514870 Update Dependency: SelectCategoriesValues 127 | [ImmutableUI][AsyncListener]: 0x0000600000514870 Update Output: SelectCategoriesValues 128 | [ImmutableUI][AsyncListener]: 0x0000600001c34ee0 Update: SelectCategoriesStatus 129 | [ImmutableUI][AsyncListener]: 0x0000600001c34ee0 Update Output: SelectCategoriesStatus 130 | [ImmutableUI][AsyncListener]: 0x00006000005150e0 Update: SelectAnimalsValues(categoryId: nil) 131 | [ImmutableUI][AsyncListener]: 0x00006000005150e0 Update Dependency: SelectAnimalsValues(categoryId: nil) 132 | [ImmutableUI][AsyncListener]: 0x00006000005150e0 Update Output: SelectAnimalsValues(categoryId: nil) 133 | [ImmutableUI][AsyncListener]: 0x000060000190e980 Update: SelectCategory(categoryId: nil) 134 | [ImmutableUI][AsyncListener]: 0x000060000190e980 Update Output: SelectCategory(categoryId: nil) 135 | [ImmutableUI][AsyncListener]: 0x0000600001c35ab0 Update: SelectAnimalsStatus 136 | [ImmutableUI][AsyncListener]: 0x0000600001c35ab0 Update Output: SelectAnimalsStatus 137 | [ImmutableUI][AsyncListener]: 0x00006000006300a0 Update: SelectAnimal(animalId: nil) 138 | [ImmutableUI][AsyncListener]: 0x00006000006300a0 Update Output: SelectAnimal(animalId: nil) 139 | [ImmutableUI][AsyncListener]: 0x000060000190ba80 Update: SelectCategory(animalId: nil) 140 | [ImmutableUI][AsyncListener]: 0x000060000190ba80 Update Output: SelectCategory(animalId: nil) 141 | [ImmutableUI][AsyncListener]: 0x0000600001c20070 Update: SelectAnimalStatus(animalId: nil) 142 | [ImmutableUI][AsyncListener]: 0x0000600001c20070 Update Output: SelectAnimalStatus(animalId: nil) 143 | [ImmutableUI][AsyncListener]: 0x0000600001c34ee0 Update: SelectCategoriesStatus 144 | [ImmutableUI][AsyncListener]: 0x0000600001c34ee0 Update Output: SelectCategoriesStatus 145 | [ImmutableUI][AsyncListener]: 0x0000600001c35ab0 Update: SelectAnimalsStatus 146 | [ImmutableUI][AsyncListener]: 0x0000600001c35ab0 Update Output: SelectAnimalsStatus 147 | [ImmutableUI][AsyncListener]: 0x00006000006300a0 Update: SelectAnimal(animalId: nil) 148 | [ImmutableUI][AsyncListener]: 0x00006000006300a0 Update Output: SelectAnimal(animalId: nil) 149 | [ImmutableUI][AsyncListener]: 0x000060000190ba80 Update: SelectCategory(animalId: nil) 150 | [ImmutableUI][AsyncListener]: 0x000060000190ba80 Update Output: SelectCategory(animalId: nil) 151 | [ImmutableUI][AsyncListener]: 0x0000600001c20070 Update: SelectAnimalStatus(animalId: nil) 152 | [ImmutableUI][AsyncListener]: 0x0000600001c20070 Update Output: SelectAnimalStatus(animalId: nil) 153 | [ImmutableUI][AsyncListener]: 0x000060000190e980 Update: SelectCategory(categoryId: nil) 154 | [ImmutableUI][AsyncListener]: 0x000060000190e980 Update Output: SelectCategory(categoryId: nil) 155 | [ImmutableUI][AsyncListener]: 0x0000600001c35ab0 Update: SelectAnimalsStatus 156 | [ImmutableUI][AsyncListener]: 0x0000600001c35ab0 Update Output: SelectAnimalsStatus 157 | [ImmutableUI][AsyncListener]: 0x0000600000514870 Update: SelectCategoriesValues 158 | [ImmutableUI][AsyncListener]: 0x0000600000514870 Update Dependency: SelectCategoriesValues 159 | [ImmutableUI][AsyncListener]: 0x0000600000514870 Update Output: SelectCategoriesValues 160 | [ImmutableUI][AsyncListener]: 0x000060000190ba80 Update: SelectCategory(animalId: nil) 161 | [ImmutableUI][AsyncListener]: 0x000060000190ba80 Update Output: SelectCategory(animalId: nil) 162 | [ImmutableUI][AsyncListener]: 0x000060000190e980 Update: SelectCategory(categoryId: nil) 163 | [ImmutableUI][AsyncListener]: 0x000060000190e980 Update Output: SelectCategory(categoryId: nil) 164 | [ImmutableUI][AsyncListener]: 0x00006000006300a0 Update: SelectAnimal(animalId: nil) 165 | [ImmutableUI][AsyncListener]: 0x00006000006300a0 Update Output: SelectAnimal(animalId: nil) 166 | [ImmutableUI][AsyncListener]: 0x0000600001c20070 Update: SelectAnimalStatus(animalId: nil) 167 | [ImmutableUI][AsyncListener]: 0x0000600001c20070 Update Output: SelectAnimalStatus(animalId: nil) 168 | [ImmutableUI][AsyncListener]: 0x0000600001c34ee0 Update: SelectCategoriesStatus 169 | [ImmutableUI][AsyncListener]: 0x0000600001c34ee0 Update Output: SelectCategoriesStatus 170 | ``` 171 | 172 | These look like a lot of selectors to launch our application, but most of these run in constant time. The only selectors here that run above-constant time are `SelectCategoriesValues` and `SelectAnimalsValues`, and `SelectAnimalsValues` returns an empty `Array` in constant time when we pass `nil` for `Category.ID`. Our first `SelectCategoriesValues` returns an empty `Array`; there is no time spent sorting because there are no `Category` values before our asynchronous fetch operations has completed. The good news here is displaying our components performs just one `O(n log n)` operation. 173 | 174 | Our focus is on teaching the `ImmutableData` architecture. We do teach how to leverage SwiftData, but a complete tutorial on debugging and optimizing SwiftData is outside the scope of this tutorial. We would like to recommend two more launch arguments you might be interested in: 175 | 176 | ```text 177 | -com.apple.CoreData.SQLDebug 1 178 | -com.apple.CoreData.ConcurrencyDebug 1 179 | ``` 180 | 181 | As of this writing, these launch arguments — which were originally built for CoreData — enable extra logging and stricter concurrency checking.[^2] Try these for yourself as you build products on SwiftData to help defend against bugs before they ship in your production application. 182 | 183 | We completed two sample products: Counter and Animals. Both applications are built from `ImmutableData`. We specify our product domain when we construct our `Store`, but the `ImmutableData` infra *itself* did not change. Our infra deployed to both products without additional work. We’re going to use that same infra — without making changes to that infra — to begin a new sample product in our next chapter. 184 | 185 | [^1]: https://www.avanderlee.com/xcode/overriding-userdefaults-launch-arguments/ 186 | [^2]: https://useyourloaf.com/blog/debugging-core-data/ 187 | -------------------------------------------------------------------------------- /Chapters/Chapter-11.md: -------------------------------------------------------------------------------- 1 | # QuakesDataClient 2 | 3 | Similar to our `AnimalsDataClient` executable, we can build a `QuakesDataClient` executable for testing against a “real” SwiftData database. Select the `QuakesData` package and open `Sources/QuakesDataClient/main.swift`. 4 | 5 | Here is our work to construct a `LocalStore` and a `RemoteStore`: 6 | 7 | ```swift 8 | // main.swift 9 | 10 | import Foundation 11 | import QuakesData 12 | import Services 13 | 14 | extension NetworkSession: RemoteStoreNetworkSession { 15 | 16 | } 17 | 18 | func makeLocalStore() throws -> LocalStore { 19 | if let url = Process().currentDirectoryURL?.appending( 20 | component: "default.store", 21 | directoryHint: .notDirectory 22 | ) { 23 | return try LocalStore(url: url) 24 | } 25 | return try LocalStore() 26 | } 27 | 28 | func makeRemoteStore() -> RemoteStore> { 29 | let session = NetworkSession(urlSession: URLSession.shared) 30 | return RemoteStore(session: session) 31 | } 32 | 33 | func main() async throws { 34 | let localStore = try makeLocalStore() 35 | let remoteStore = makeRemoteStore() 36 | 37 | let localQuakes = try await localStore.fetchLocalQuakesQuery() 38 | print(localQuakes) 39 | 40 | let remoteQuakes = try await remoteStore.fetchRemoteQuakesQuery(range: .allHour) 41 | print(remoteQuakes) 42 | } 43 | 44 | try await main() 45 | ``` 46 | 47 | The `Services` module is provided along with our Workspace from the [`ImmutableData-Samples`](https://github.com/Swift-ImmutableData/ImmutableData-Samples) repo. This module provides a `NetworkSession` class that simplifies fetching and serializing data models from a remote server. Our goal with this tutorial is to teach the `ImmutableData` architecture. Networking code is interesting, but does not really need to block that main goal. You are welcome to explore other solutions for networking in your own products. We provide the `Services` module just to keep things easy for our tutorial. 48 | 49 | Once we have a `NetworkSession` instance, we use that to create a `RemoteStore`. We create a `LocalStore` using a similar pattern to what we saw in `AnimalsDataClient`. 50 | 51 | Our `main` function constructs a `LocalStore` and a `RemoteStore`. We now have the option to begin performing queries and mutations. Try it for yourself. You can fetch earthquakes from USGS, insert those earthquakes in a local database, then run your executable again and confirm the mutations were persisted. 52 | -------------------------------------------------------------------------------- /Chapters/Chapter-13.md: -------------------------------------------------------------------------------- 1 | # Quakes.app 2 | 3 | There are only a few steps left before we can launch our application. Select `Quakes.xcodeproj` and open `QuakesApp.swift`. We can delete the original “Hello World” template. Let’s begin by defining our `QuakesApp` type: 4 | 5 | ```swift 6 | // QuakesApp.swift 7 | 8 | import ImmutableData 9 | import ImmutableUI 10 | import QuakesData 11 | import QuakesUI 12 | import Services 13 | import SwiftUI 14 | 15 | @main @MainActor struct QuakesApp { 16 | @State private var store = Store( 17 | initialState: QuakesState(), 18 | reducer: QuakesReducer.reduce 19 | ) 20 | @State private var listener = Listener( 21 | localStore: Self.makeLocalStore(), 22 | remoteStore: Self.makeRemoteStore() 23 | ) 24 | 25 | init() { 26 | self.listener.listen(to: self.store) 27 | } 28 | } 29 | ``` 30 | 31 | We construct our `QuakesApp` with a `Store` and a `Listener`. Our `Store` is constructed with `QuakesState` and `QuakesReducer`. Our `Listener` will be constructed with a `LocalStore` and a `RemoteStore`. 32 | 33 | Here is where we construct our `LocalStore`: 34 | 35 | ```swift 36 | // QuakesApp.swift 37 | 38 | extension QuakesApp { 39 | private static func makeLocalStore() -> LocalStore { 40 | do { 41 | return try LocalStore() 42 | } catch { 43 | fatalError("\(error)") 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | Here is where we construct our `RemoteStore`: 50 | 51 | ```swift 52 | // QuakesApp.swift 53 | 54 | extension NetworkSession: @retroactive RemoteStoreNetworkSession { 55 | 56 | } 57 | 58 | extension QuakesApp { 59 | private static func makeRemoteStore() -> RemoteStore> { 60 | let session = NetworkSession(urlSession: URLSession.shared) 61 | return RemoteStore(session: session) 62 | } 63 | } 64 | ``` 65 | 66 | We saw `NetworkSession` when we built our `QuakesDataClient` executable. It’s a lightweight wrapper around `URLSession` for requesting and serializing JSON. Our `RemoteStore` needs a type that adopts `RemoteStoreNetworkSession`: we can extend `NetworkSession` to conform to `RemoteStoreNetworkSession`. The `retroactive` attribute can silence a compiler warning.[^1] It’s a legit warning, but we control `NetworkSession` and `QuakesApp`; there’s not much of a risk of this breaking anything for our tutorial. 67 | 68 | Here is our `body` property: 69 | 70 | ```swift 71 | // QuakesApp.swift 72 | 73 | extension QuakesApp: App { 74 | var body: some Scene { 75 | WindowGroup { 76 | Provider(self.store) { 77 | Content() 78 | } 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | We can now build and run (`⌘ R`). Our application is now a working clone of the original Quakes sample application from Apple. We preserved the core functionality: we launch our app and fetch `Quake` values from the USGS server. We can filter and sort `Quake` values. Our `List` and `Map` components stay updated with the same subset of `Quake` values. We also save our global state on our filesystem in a local database. Launching the app a second time loads the `Quake` values that were previously cached. We also have the option to delete `Quake` values from our local database. 85 | 86 | You can also create a new window (`⌘ N`) and watch as edits to global state from one window are reflected in the other. The global state is the complete set of `Quake` values: this is consistent. The local state is tracked independently: users can sort and filter on one window without affecting the sort and filter options of other windows. 87 | 88 | Similar to our Animals product, we can also use Launch Arguments to see the `print` statements we added for debugging: 89 | 90 | ```text 91 | -com.northbronson.QuakesData.Debug 1 92 | -com.northbronson.QuakesUI.Debug 1 93 | -com.northbronson.ImmutableUI.Debug 1 94 | -com.apple.CoreData.SQLDebug 1 95 | -com.apple.CoreData.ConcurrencyDebug 1 96 | ``` 97 | 98 | It’s easy to save thousands of `Quake` values in global state. Turning on `com.northbronson.QuakesData.Debug` will print *a lot* of data. If you were interested in trying to reduce the amount of data printed, you could try and update `QuakesData.Listener` to print only the *difference* between the state of our system before and after our Reducer returned. Instead of printing the old state and the new state, think about an algorithm to compare two state values and only print the diff. 99 | 100 | Go ahead and try launching the original sample project from Apple. The original sample project fetched all the earthquake values recorded during the current day. a typical day might be about 300 to 400 earthquake values. To test performance with more data, we hacked the original sample project to fetch all the earthquake values recording during the current month. Now, we are fetching an order of magnitude more earthquake values: about 10K. Depending on your network connection, the work to download that data from USGS is not the major bottleneck. The `URLSession` operation from the original sample project is asynchronous and concurrent. Our `main` thread is not blocked: we can still scroll around our `Map` component, even if we don’t have any data to display. Where things get difficult is when those earthquake values have returned from USGS. We then iterate through and insert new `PersistentModel` reference in our `ModelContext`. This work happens on `main`, and it’s slow. *This* is the bottleneck that leads to apps freezing and “beachballs” spinning. 101 | 102 | To be fair, there are more advanced techniques that can perform asynchronous and concurrent `ModelContext` operations without blocking `main`.[^2] Similar to CoreData, we can then attempt to “sync” a `ModelContext` tied to `main` with a `ModelContext` performing asynchronous and concurrent operations. This can help improve performance, but it’s complex code; we don’t want our product engineers to have to think about this. Even if we *were* able to factor all this code down into shared infra and we *were* able to write all the automated tests and we *were* able to defend against all the edge-cases, we would still be stuck with a programming model we don’t agree with: product engineers would be performing imperative mutations on mutable objects from their component tree. 103 | 104 | Migrating to `ImmutableData` fixes both these problems for us: it’s an “abstraction layer” that makes it easy to keep expensive SwiftData operations asynchronous and concurrent, and it delivers immutable value types to our component tree. 105 | 106 | [^1]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0364-retroactive-conformance-warning.md 107 | [^2]: https://fatbobman.com/en/posts/concurret-programming-in-swiftdata/ 108 | -------------------------------------------------------------------------------- /Chapters/Chapter-14.md: -------------------------------------------------------------------------------- 1 | # AnimalsData 2 | 3 | One of the realities of the practice of Software Engineering is the inevitable “changing requirements”.[^1] We build an application, customers need a new feature, and we have to build something. Sometimes these customers are external customers: the actual users that paid for our product. Sometimes these customers are internal customers: our product managers or engineering managers. 4 | 5 | Our `ImmutableData` architecture looks good so far for “greenfield” products: three new sample products we built from scratch. What happens *after* the product is built? How flexible are these products to adapt to changing requirements? 6 | 7 | Our Animals product was built to save data to a local database. Our experiment will be to build a “next-gen” Animals product. The changing requirements are we now need to support saving data to a remote server. We don’t have to cache data locally; the server will be our “source of truth”. 8 | 9 | In a world where our SwiftUI application is built directly on SwiftData, this is possible using some advanced techniques with `DataStore`.[^2] This was also possible in Core Data using `NSIncrementalStore`.[^3] We’re going to take a slightly different approach. We’re not going to update our existing `LocalStore` to forward its queries and mutations to a server. We’re going to build a `RemoteStore` and replace our existing `LocalStore` on app launch. 10 | 11 | ## Category 12 | 13 | Our remote server will return JSON. To serialize that JSON to and from `Category` values, we are going to adopt `Codable`. Select the `AnimalsData` package and open `Sources/AnimalsData/Category.swift`. Add the `Codable` conformance to our main declaration: 14 | 15 | ```swift 16 | // Category.swift 17 | 18 | public struct Category: Hashable, Codable, Sendable { 19 | public let categoryId: String 20 | public let name: String 21 | 22 | package init( 23 | categoryId: String, 24 | name: String 25 | ) { 26 | self.categoryId = categoryId 27 | self.name = name 28 | } 29 | } 30 | ``` 31 | 32 | ## Animal 33 | 34 | Let’s update `Animal` to adopt `Codable`. Open `Sources/AnimalsData/Animal.swift`. Add the `Codable` conformance to our main declaration: 35 | 36 | ```swift 37 | // Animal.swift 38 | 39 | public struct Animal: Hashable, Codable, Sendable { 40 | public let animalId: String 41 | public let name: String 42 | public let diet: Diet 43 | public let categoryId: String 44 | 45 | package init( 46 | animalId: String, 47 | name: String, 48 | diet: Diet, 49 | categoryId: String 50 | ) { 51 | self.animalId = animalId 52 | self.name = name 53 | self.diet = diet 54 | self.categoryId = categoryId 55 | } 56 | } 57 | ``` 58 | 59 | We also add `Codable` to the `Diet` declaration: 60 | 61 | ```swift 62 | // Animal.swift 63 | 64 | extension Animal { 65 | public enum Diet: String, CaseIterable, Hashable, Codable, Sendable { 66 | case herbivorous = "Herbivore" 67 | case carnivorous = "Carnivore" 68 | case omnivorous = "Omnivore" 69 | } 70 | } 71 | ``` 72 | 73 | ## RemoteStore 74 | 75 | Our `PersistentSession` performs asynchronous queries and mutations on a type that conforms to `PersistentSessionPersistentStore`. Our `PersistentSession` does not have any *direct* knowledge of SwiftData: that knowledge lives in `LocalStore`. We’re going to build a new type that conforms to `PersistentSessionPersistentStore`. This type will perform asynchronous queries and mutations against a remote server. 76 | 77 | There’s no “one right way” to design a remote API. This is an interesting topic, but it’s orthogonal to our main goal of teaching `ImmutableData`. We’re going to build a remote server with an API inspired by GraphQL.[^4] The history of GraphQL is closely tied with the history of Relay.[^5] Relay evolved out of the Flux Architecture with an emphasis on retrieving server data.[^6] Relay and Redux evolved independently of each other, but they both share a common ancestor: Flux. 78 | 79 | This isn’t meant as a strong opinion about your own products built from `ImmutableData`. There’s actually nothing in our `RemoteStore` that will need any direct knowledge of the `ImmutableData` architecture. If your server engineers build something that looks like “classic REST”, that doesn’t need to block you on shipping with `ImmutableData`. 80 | 81 | Add a new Swift file under `Sources/AnimalsData`. Name this file `RemoteStore.swift`. 82 | 83 | Let’s begin with a type to define our Request. This is the outgoing communication from our client to our server. 84 | 85 | ```swift 86 | // RemoteStore.swift 87 | 88 | import Foundation 89 | 90 | package struct RemoteRequest: Hashable, Codable, Sendable { 91 | package let query: Array? 92 | package let mutation: Array? 93 | 94 | package init( 95 | query: Array? = nil, 96 | mutation: Array? = nil 97 | ) { 98 | self.query = query 99 | self.mutation = mutation 100 | } 101 | } 102 | ``` 103 | 104 | Our `RemoteRequest` is constructed with two parameters: an `Array` of `Query` values and an `Array` of `Mutation` values. We will use `Query` values to indicate operations to read data and `Mutation` values to indicate operations to write data. 105 | 106 | We will define two possible queries: 107 | 108 | ```swift 109 | // RemoteStore.swift 110 | 111 | extension RemoteRequest { 112 | package enum Query: Hashable, Codable, Sendable { 113 | case categories 114 | case animals 115 | } 116 | } 117 | ``` 118 | 119 | This is easy: one option to request `Animal` values and one option to request `Category` values. 120 | 121 | We will define four possible mutations: 122 | 123 | ```swift 124 | // RemoteStore.swift 125 | 126 | extension RemoteRequest { 127 | package enum Mutation: Hashable, Codable, Sendable { 128 | case addAnimal( 129 | name: String, 130 | diet: Animal.Diet, 131 | categoryId: String 132 | ) 133 | case updateAnimal( 134 | animalId: String, 135 | name: String, 136 | diet: Animal.Diet, 137 | categoryId: String 138 | ) 139 | case deleteAnimal(animalId: String) 140 | case reloadSampleData 141 | } 142 | } 143 | ``` 144 | 145 | This should look familiar: these look a lot like the two queries and four mutations we defined on `PersistentSessionPersistentStore`. 146 | 147 | There are two more constructors that will make things easier for us when we only need one `Query` or one `Mutation`: 148 | 149 | ```swift 150 | // RemoteStore.swift 151 | 152 | extension RemoteRequest { 153 | fileprivate init(query: Query) { 154 | self.init( 155 | query: [ 156 | query 157 | ] 158 | ) 159 | } 160 | } 161 | 162 | extension RemoteRequest { 163 | fileprivate init(mutation: Mutation) { 164 | self.init( 165 | mutation: [ 166 | mutation 167 | ] 168 | ) 169 | } 170 | } 171 | ``` 172 | 173 | Let’s build a type to define our Response. This is the incoming communication from our server to our client. 174 | 175 | ```swift 176 | // RemoteStore.swift 177 | 178 | package struct RemoteResponse: Hashable, Codable, Sendable { 179 | package let query: Array? 180 | package let mutation: Array? 181 | 182 | package init( 183 | query: Array? = nil, 184 | mutation: Array? = nil 185 | ) { 186 | self.query = query 187 | self.mutation = mutation 188 | } 189 | } 190 | ``` 191 | 192 | Our `RemoteResponse` is constructed with two parameters: an `Array` of `Query` values and an `Array` of `Mutation` values. 193 | 194 | We will define two possible queries: 195 | 196 | ```swift 197 | // RemoteStore.swift 198 | 199 | extension RemoteResponse { 200 | package enum Query: Hashable, Codable, Sendable { 201 | case categories(categories: Array) 202 | case animals(animals: Array) 203 | } 204 | } 205 | ``` 206 | 207 | We will define four possible mutations: 208 | 209 | ```swift 210 | // RemoteStore.swift 211 | 212 | extension RemoteResponse { 213 | package enum Mutation: Hashable, Codable, Sendable { 214 | case addAnimal(animal: Animal) 215 | case updateAnimal(animal: Animal) 216 | case deleteAnimal(animal: Animal) 217 | case reloadSampleData( 218 | animals: Array, 219 | categories: Array 220 | ) 221 | } 222 | } 223 | ``` 224 | 225 | We also want some `Error` types if the response from our server is missing data: 226 | 227 | ```swift 228 | // RemoteStore.swift 229 | 230 | extension RemoteResponse.Query { 231 | package struct Error: Swift.Error { 232 | package enum Code: Equatable { 233 | case categoriesNotFound 234 | case animalsNotFound 235 | } 236 | 237 | package let code: Self.Code 238 | } 239 | } 240 | 241 | extension RemoteResponse.Mutation { 242 | package struct Error: Swift.Error { 243 | package enum Code: Equatable { 244 | case animalNotFound 245 | case sampleDataNotFound 246 | } 247 | 248 | package let code: Self.Code 249 | } 250 | } 251 | ``` 252 | 253 | Similar to `QuakesData.RemoteStore`, we’re going to define a protocol for our network session. We don’t want our `RemoteStore` to explicitly depend on `URLSession` or any type that performs “real” networking; we want the ability to inject a test-double to return stub data in tests. 254 | 255 | ```swift 256 | // RemoteStore.swift 257 | 258 | public protocol RemoteStoreNetworkSession: Sendable { 259 | func json( 260 | for request: URLRequest, 261 | from decoder: JSONDecoder 262 | ) async throws -> T where T : Decodable 263 | } 264 | ``` 265 | 266 | Here is the main declaration of our `RemoteStore`: 267 | 268 | ```swift 269 | // RemoteStore.swift 270 | 271 | final public actor RemoteStore: PersistentSessionPersistentStore where NetworkSession : RemoteStoreNetworkSession { 272 | private let session: NetworkSession 273 | 274 | public init(session: NetworkSession) { 275 | self.session = session 276 | } 277 | } 278 | ``` 279 | 280 | We’re going to write a utility to serialize our `RemoteRequest` to a `URLRequest` that can be forwarded to our `NetworkSession`: 281 | 282 | ```swift 283 | // RemoteStore.swift 284 | 285 | extension RemoteStore { 286 | package struct Error : Swift.Error { 287 | package enum Code: Equatable { 288 | case urlError 289 | case requestError 290 | } 291 | 292 | package let code: Self.Code 293 | } 294 | } 295 | 296 | extension RemoteStore { 297 | private static func networkRequest(remoteRequest: RemoteRequest) throws -> URLRequest { 298 | guard 299 | let url = URL(string: "http://localhost:8080/animals/api") 300 | else { 301 | throw Error(code: .urlError) 302 | } 303 | var networkRequest = URLRequest(url: url) 304 | networkRequest.httpMethod = "POST" 305 | networkRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") 306 | networkRequest.httpBody = try { 307 | do { 308 | return try JSONEncoder().encode(remoteRequest) 309 | } catch { 310 | throw Error(code: .requestError) 311 | } 312 | }() 313 | return networkRequest 314 | } 315 | } 316 | ``` 317 | 318 | In the next chapter, we will use Vapor to build a server that runs on `localhost`. The `URL` endpoint will accept a `POST` request and return JSON. 319 | 320 | We can now add the functions to conform to `PersistentSessionPersistentStore`. Let’s begin with `fetchCategoriesQuery`: 321 | 322 | ```swift 323 | // RemoteStore.swift 324 | 325 | extension RemoteStore { 326 | public func fetchCategoriesQuery() async throws -> Array { 327 | let remoteRequest = RemoteRequest(query: .categories) 328 | let networkRequest = try Self.networkRequest(remoteRequest: remoteRequest) 329 | let remoteResponse: RemoteResponse = try await self.session.json( 330 | for: networkRequest, 331 | from: JSONDecoder() 332 | ) 333 | guard 334 | let query = remoteResponse.query, 335 | let categories = { 336 | let element = query.first { element in 337 | if case .categories = element { 338 | return true 339 | } 340 | return false 341 | } 342 | if case .categories(categories: let categories) = element { 343 | return categories 344 | } 345 | return nil 346 | }() 347 | else { 348 | throw RemoteResponse.Query.Error(code: .categoriesNotFound) 349 | } 350 | return categories 351 | } 352 | } 353 | ``` 354 | 355 | This might look like a lot of code, but we can think through things step-by-step: 356 | 357 | * We construct a `RemoteRequest` with `Query.categories`. 358 | * We transform our `RemoteRequest` to a `URLRequest`. 359 | * We forward our `URLRequest` to our `NetworkSession` and `await` a `RemoteResponse`. 360 | * We look inside our `RemoteResponse` for a `Query.categories` value. If we found a `Query.categories` value, we return the `Array` of `Category` values returned by our server. If this values was missing, we throw an `Error`. We can assume that our server would return at most one `Query.categories` value — we don’t need to code around two different `Query.categories` values returned in the same `RemoteResponse`. 361 | 362 | Here is a similar pattern for `fetchAnimalsQuery`: 363 | 364 | ```swift 365 | // RemoteStore.swift 366 | 367 | extension RemoteStore { 368 | public func fetchAnimalsQuery() async throws -> Array { 369 | let remoteRequest = RemoteRequest(query: .animals) 370 | let networkRequest = try Self.networkRequest(remoteRequest: remoteRequest) 371 | let remoteResponse: RemoteResponse = try await self.session.json( 372 | for: networkRequest, 373 | from: JSONDecoder() 374 | ) 375 | guard 376 | let query = remoteResponse.query, 377 | let animals = { 378 | let element = query.first { element in 379 | if case .animals = element { 380 | return true 381 | } 382 | return false 383 | } 384 | if case .animals(animals: let animals) = element { 385 | return animals 386 | } 387 | return nil 388 | }() 389 | else { 390 | throw RemoteResponse.Query.Error(code: .animalsNotFound) 391 | } 392 | return animals 393 | } 394 | } 395 | ``` 396 | 397 | On app launch, we will perform a `fetchCategoriesQuery` *and* a `fetchAnimalsQuery`. This will be two different network requests. A legit optimization would be to add more code in our `Listener` to call only one query on app launch; something like an `appLaunchQuery`. We can then make one network request for `Query.categories` and `Query.animals` at the same time: our server would return them both together. 398 | 399 | Mutations will follow a similar pattern. Here is `addAnimalMutation`: 400 | 401 | ```swift 402 | // RemoteStore.swift 403 | 404 | extension RemoteStore { 405 | public func addAnimalMutation( 406 | name: String, 407 | diet: Animal.Diet, 408 | categoryId: String 409 | ) async throws -> Animal { 410 | let remoteRequest = RemoteRequest( 411 | mutation: .addAnimal( 412 | name: name, 413 | diet: diet, 414 | categoryId: categoryId 415 | ) 416 | ) 417 | let networkRequest = try Self.networkRequest(remoteRequest: remoteRequest) 418 | let remoteResponse: RemoteResponse = try await self.session.json( 419 | for: networkRequest, 420 | from: JSONDecoder() 421 | ) 422 | guard 423 | let mutation = remoteResponse.mutation, 424 | let animal = { 425 | let element = mutation.first { element in 426 | if case .addAnimal = element { 427 | return true 428 | } 429 | return false 430 | } 431 | if case .addAnimal(animal: let animal) = element { 432 | return animal 433 | } 434 | return nil 435 | }() 436 | else { 437 | throw RemoteResponse.Mutation.Error(code: .animalNotFound) 438 | } 439 | return animal 440 | } 441 | } 442 | ``` 443 | 444 | Here is `updateAnimalMutation`: 445 | 446 | ```swift 447 | // RemoteStore.swift 448 | 449 | extension RemoteStore { 450 | public func updateAnimalMutation( 451 | animalId: String, 452 | name: String, 453 | diet: Animal.Diet, 454 | categoryId: String 455 | ) async throws -> Animal { 456 | let remoteRequest = RemoteRequest( 457 | mutation: .updateAnimal( 458 | animalId: animalId, 459 | name: name, 460 | diet: diet, 461 | categoryId: categoryId 462 | ) 463 | ) 464 | let networkRequest = try Self.networkRequest(remoteRequest: remoteRequest) 465 | let remoteResponse: RemoteResponse = try await self.session.json( 466 | for: networkRequest, 467 | from: JSONDecoder() 468 | ) 469 | guard 470 | let mutation = remoteResponse.mutation, 471 | let animal = { 472 | let element = mutation.first { element in 473 | if case .updateAnimal = element { 474 | return true 475 | } 476 | return false 477 | } 478 | if case .updateAnimal(animal: let animal) = element { 479 | return animal 480 | } 481 | return nil 482 | }() 483 | else { 484 | throw RemoteResponse.Mutation.Error(code: .animalNotFound) 485 | } 486 | return animal 487 | } 488 | } 489 | ``` 490 | 491 | Here is `deleteAnimalMutation`: 492 | 493 | ```swift 494 | // RemoteStore.swift 495 | 496 | extension RemoteStore { 497 | public func deleteAnimalMutation(animalId: String) async throws -> Animal { 498 | let remoteRequest = RemoteRequest( 499 | mutation: .deleteAnimal(animalId: animalId) 500 | ) 501 | let networkRequest = try Self.networkRequest(remoteRequest: remoteRequest) 502 | let remoteResponse: RemoteResponse = try await self.session.json( 503 | for: networkRequest, 504 | from: JSONDecoder() 505 | ) 506 | guard 507 | let mutation = remoteResponse.mutation, 508 | let animal = { 509 | let element = mutation.first { element in 510 | if case .deleteAnimal = element { 511 | return true 512 | } 513 | return false 514 | } 515 | if case .deleteAnimal(animal: let animal) = element { 516 | return animal 517 | } 518 | return nil 519 | }() 520 | else { 521 | throw RemoteResponse.Mutation.Error(code: .animalNotFound) 522 | } 523 | return animal 524 | } 525 | } 526 | ``` 527 | 528 | Here is `reloadSampleDataMutation`: 529 | 530 | ```swift 531 | // RemoteStore.swift 532 | 533 | extension RemoteStore { 534 | public func reloadSampleDataMutation() async throws -> ( 535 | animals: Array, 536 | categories: Array 537 | ) { 538 | let remoteRequest = RemoteRequest( 539 | mutation: .reloadSampleData 540 | ) 541 | let networkRequest = try Self.networkRequest(remoteRequest: remoteRequest) 542 | let remoteResponse: RemoteResponse = try await self.session.json( 543 | for: networkRequest, 544 | from: JSONDecoder() 545 | ) 546 | guard 547 | let mutation = remoteResponse.mutation, 548 | let (animals, categories) = { 549 | let element = mutation.first { element in 550 | if case .reloadSampleData = element { 551 | return true 552 | } 553 | return false 554 | } 555 | if case .reloadSampleData(animals: let animals, categories: let categories) = element { 556 | return (animals, categories) 557 | } 558 | return nil 559 | }() 560 | else { 561 | throw RemoteResponse.Mutation.Error(code: .sampleDataNotFound) 562 | } 563 | return (animals, categories) 564 | } 565 | } 566 | ``` 567 | 568 | Over the course of this tutorial, we’ve presented some strong opinions and value-statements about state-management. We feel strongly about these opinions, but it’s important to remember that some of this code was “arbitrary” in the sense that the designs and patterns are orthogonal to our goal of teaching `ImmutableData`. The `ImmutableData` has strong opinions about *how* components should affect transformations on global state, but we don’t always have strong opinions about *what* that transformation should look like. 569 | 570 | Our `LocalStore` was built to persist data to our filesystem using SwiftData. We could have chosen Core Data, SQLite, or something else; it is an implementation detail that would not need to influence how we go about building apps on `ImmutableData`. The decision to choose SwiftData — and the code we wrote to interact with SwiftData — is not meant to sound like an opinion about “the right way” to use `ImmutableData`. 571 | 572 | Similarly, our `RemoteStore` is built on `URLSession` and an endpoint that presents a GraphQL-inspired API. These are arbitrary; your own products might look very different, and that’s ok. 573 | 574 | [^1]: https://martinfowler.com/distributedComputing/soft.pdf 575 | [^2]: https://developer.apple.com/videos/play/wwdc2024/10138/ 576 | [^3]: https://nshipster.com/nsincrementalstore/ 577 | [^4]: https://graphql.org/learn/ 578 | [^5]: https://engineering.fb.com/2015/09/14/core-infra/graphql-a-data-query-language/ 579 | [^6]: https://engineering.fb.com/2015/09/14/core-infra/relay-declarative-data-for-react-applications/ 580 | -------------------------------------------------------------------------------- /Chapters/Chapter-15.md: -------------------------------------------------------------------------------- 1 | # AnimalsDataServer 2 | 3 | Before we run our Animals application with `RemoteStore`, we’re going to actually need an HTTP server to read and write data. 4 | 5 | There are many options and languages to choose from when building a server. Our goal is to teach `ImmutableData`; many of the decisions that go into building scalable web services are outside the scope of this tutorial. 6 | 7 | To keep things simple, we’re going to take a couple of shortcuts: 8 | 9 | * We will use Swift to build our HTTP server. 10 | * We will run our HTTP server on `localhost`. 11 | 12 | This isn’t going to scale to millions of daily active users, and that’s ok. We’re just unblocking ourselves on testing our Animals application running against a real server. 13 | 14 | Engineers are already using Swift to build server-side applications.[^1] One of the popular repos for engineering on server-side is Vapor.[^2] We will use Vapor to build an HTTP server, run our server on `localhost`, and test our Animals application. 15 | 16 | Up to this point, the code we wrote has been almost all new. We refrained from introducing external dependencies and repos. We don’t really want there to be anything “magic” about what we are building. We built the `ImmutableData` infra and we built three sample application products against that infra. Building an HTTP server in Swift is a very specialized task. Learning how to build this technology ourselves might be interesting, but it should not block our goal of teaching `ImmutableData`. We’re going to use Vapor to move fast. 17 | 18 | We’re also going to import the [`Swift-Async-Algorithms`][^3] repo from Apple. This is helpful for when we iterate over a sequence of values that perform asynchronous operations. 19 | 20 | Our Animals product has the ability to read data with queries and write data with mutations. We would like our server to persist that data across launches. The Vapor ecosystem ships Fluent for persisting data in a database. We’re going to take a shortcut. Instead of learning how Fluent works, let’s just use our `LocalStore`. 21 | 22 | Select the `AnimalsData` package and open `Sources/AnimalsDataServer/main.swift`. Let’s begin with some utilities for mapping an asynchronous operation over a sequence of values: 23 | 24 | ```swift 25 | // main.swift 26 | 27 | import AnimalsData 28 | import AsyncAlgorithms 29 | import Foundation 30 | import Vapor 31 | 32 | extension Sequence { 33 | public func map(_ transform: @escaping @Sendable (Self.Element) async throws -> Transformed) async rethrows -> Array { 34 | try await self.async.map(transform) 35 | } 36 | } 37 | 38 | extension AsyncSequence { 39 | fileprivate func map(_ transform: @escaping @Sendable (Self.Element) async throws -> Transformed) async rethrows -> Array { 40 | let map: AsyncThrowingMapSequence = self.map(transform) 41 | return try await Array(map) 42 | } 43 | } 44 | ``` 45 | 46 | Let’s add some utilities on `LocalStore` for transforming a `RemoteRequest` to a `RemoteResponse`: 47 | 48 | ```swift 49 | // main.swift 50 | 51 | extension LocalStore { 52 | fileprivate func response(request: RemoteRequest) async throws -> RemoteResponse { 53 | RemoteResponse( 54 | query: try await self.response(query: request.query), 55 | mutation: try await self.response(mutation: request.mutation) 56 | ) 57 | } 58 | } 59 | ``` 60 | 61 | Our `RemoteRequest` was sent with an `Array` of `Query` values and an `Array` of `Mutation` values. We’re going to build a `RemoteResponse` with the data needed for our `RemoteStore`. 62 | 63 | Here is how we build the `query` and the `mutation` of our `RemoteResponse`: 64 | 65 | ```swift 66 | // main.swift 67 | 68 | extension LocalStore { 69 | private func response(query: Array?) async throws -> Array? { 70 | try await query?.map { query in try await self.response(query: query) } 71 | } 72 | } 73 | 74 | extension LocalStore { 75 | private func response(mutation: Array?) async throws -> Array? { 76 | try await mutation?.map { mutation in try await self.response(mutation: mutation) } 77 | } 78 | } 79 | ``` 80 | 81 | We need to transform a `RemoteRequest.Query` value to a `RemoteResponse.Query`: 82 | 83 | ```swift 84 | // main.swift 85 | 86 | extension LocalStore { 87 | private func response(query: RemoteRequest.Query) async throws -> RemoteResponse.Query { 88 | switch query { 89 | case .animals: 90 | let animals = try await self.fetchAnimalsQuery() 91 | return .animals(animals: animals) 92 | case .categories: 93 | let categories = try await self.fetchCategoriesQuery() 94 | return .categories(categories: categories) 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | We need to transform a `RemoteRequest.Mutation` value to a `RemoteResponse.Mutation`: 101 | 102 | ```swift 103 | // main.swift 104 | 105 | extension LocalStore { 106 | private func response(mutation: RemoteRequest.Mutation) async throws -> RemoteResponse.Mutation { 107 | switch mutation { 108 | case .addAnimal(name: let name, diet: let diet, categoryId: let categoryId): 109 | let animal = try await self.addAnimalMutation(name: name, diet: diet, categoryId: categoryId) 110 | return .addAnimal(animal: animal) 111 | case .updateAnimal(animalId: let animalId, name: let name, diet: let diet, categoryId: let categoryId): 112 | let animal = try await self.updateAnimalMutation(animalId: animalId, name: name, diet: diet, categoryId: categoryId) 113 | return .updateAnimal(animal: animal) 114 | case .deleteAnimal(animalId: let animalId): 115 | let animal = try await self.deleteAnimalMutation(animalId: animalId) 116 | return .deleteAnimal(animal: animal) 117 | case .reloadSampleData: 118 | let (animals, categories) = try await self.reloadSampleDataMutation() 119 | return .reloadSampleData(animals: animals, categories: categories) 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | Let’s construct a `LocalStore`: 126 | 127 | ```swift 128 | // main.swift 129 | 130 | func makeLocalStore() throws -> LocalStore { 131 | if let url = Process().currentDirectoryURL?.appending( 132 | component: "default.store", 133 | directoryHint: .notDirectory 134 | ) { 135 | return try LocalStore(url: url) 136 | } 137 | return try LocalStore() 138 | } 139 | ``` 140 | 141 | Let’s build our Vapor server: 142 | 143 | ```swift 144 | // main.swift 145 | 146 | func main() async throws { 147 | let localStore = try makeLocalStore() 148 | let app = try await Application.make(.detect()) 149 | app.post("animals", "api") { request in 150 | let response = Response() 151 | let remoteRequest = try request.content.decode(RemoteRequest.self) 152 | print(remoteRequest) 153 | let remoteResponse = try await localStore.response(request: remoteRequest) 154 | print(remoteResponse) 155 | try response.content.encode(remoteResponse, as: .json) 156 | return response 157 | } 158 | try await app.execute() 159 | try await app.asyncShutdown() 160 | } 161 | 162 | try await main() 163 | ``` 164 | 165 | If we build and run our executable, we can see our server is now running on `localhost`: 166 | 167 | ```shell 168 | $ swift run AnimalsDataServer 169 | [Vapor] Server started on http://127.0.0.1:8080 170 | ``` 171 | 172 | On first launch, we construct a new `LocalStore` with the sample data we built in our previous chapters. We can now run `curl` from shell and confirm the `Category` values are correct: 173 | 174 | ```shell 175 | $ curl http://localhost:8080/animals/api -X POST -d '{"query": [ {"categories": {}} ]}' -H "Content-Type: application/json" --silent | python3 -m json.tool --indent 2 176 | { 177 | "query": [ 178 | { 179 | "categories": { 180 | "categories": [ 181 | { 182 | "categoryId": "Mammal", 183 | "name": "Mammal" 184 | }, 185 | { 186 | "name": "Bird", 187 | "categoryId": "Bird" 188 | }, 189 | { 190 | "categoryId": "Amphibian", 191 | "name": "Amphibian" 192 | }, 193 | { 194 | "categoryId": "Invertebrate", 195 | "name": "Invertebrate" 196 | }, 197 | { 198 | "categoryId": "Fish", 199 | "name": "Fish" 200 | }, 201 | { 202 | "categoryId": "Reptile", 203 | "name": "Reptile" 204 | } 205 | ] 206 | } 207 | } 208 | ] 209 | } 210 | ``` 211 | 212 | We pipe our output to `python3` for pretty-printing; the original response from Vapor did not include this whitespace. 213 | 214 | Here are the `Animal` values: 215 | 216 | ```shell 217 | $ curl http://localhost:8080/animals/api -X POST -d '{"query": [ {"animals": {}} ]}' -H "Content-Type: application/json" --silent | python3 -m json.tool --indent 2 218 | { 219 | "query": [ 220 | { 221 | "animals": { 222 | "animals": [ 223 | { 224 | "diet": "Herbivore", 225 | "name": "Southern gibbon", 226 | "categoryId": "Mammal", 227 | "animalId": "Bibbon" 228 | }, 229 | { 230 | "diet": "Carnivore", 231 | "categoryId": "Amphibian", 232 | "name": "Newt", 233 | "animalId": "Newt" 234 | }, 235 | { 236 | "animalId": "Cat", 237 | "categoryId": "Mammal", 238 | "diet": "Carnivore", 239 | "name": "Cat" 240 | }, 241 | { 242 | "name": "House sparrow", 243 | "animalId": "Sparrow", 244 | "diet": "Omnivore", 245 | "categoryId": "Bird" 246 | }, 247 | { 248 | "categoryId": "Mammal", 249 | "animalId": "Kangaroo", 250 | "name": "Red kangaroo", 251 | "diet": "Herbivore" 252 | }, 253 | { 254 | "name": "Dog", 255 | "animalId": "Dog", 256 | "categoryId": "Mammal", 257 | "diet": "Carnivore" 258 | } 259 | ] 260 | } 261 | } 262 | ] 263 | } 264 | ``` 265 | 266 | We can also experiment with a mutation: 267 | 268 | ```shell 269 | $ curl http://localhost:8080/animals/api -X POST -d '{"mutation": [ {"addAnimal": {"name": "Eagle", "diet": "Carnivore", "categoryId": "Bird"}} ]}' -H "Content-Type: application/json" --silent | python3 -m json.tool --indent 2 270 | { 271 | "mutation": [ 272 | { 273 | "addAnimal": { 274 | "animal": { 275 | "animalId": "467D0044-4BB9-4C28-A10B-E87A2C328034", 276 | "diet": "Carnivore", 277 | "categoryId": "Bird", 278 | "name": "Eagle" 279 | } 280 | } 281 | } 282 | ] 283 | } 284 | ``` 285 | 286 | If we stop running our server and run our server again, we can request all the `Animal` values to confirm our mutation was saved: 287 | 288 | ```shell 289 | $ curl http://localhost:8080/animals/api -X POST -d '{"query": [ {"animals": {}} ]}' -H "Content-Type: application/json" --silent | python3 -m json.tool --indent 2 290 | { 291 | "query": [ 292 | { 293 | "animals": { 294 | "animals": [ 295 | { 296 | "animalId": "Bibbon", 297 | "diet": "Herbivore", 298 | "categoryId": "Mammal", 299 | "name": "Southern gibbon" 300 | }, 301 | { 302 | "animalId": "Newt", 303 | "name": "Newt", 304 | "diet": "Carnivore", 305 | "categoryId": "Amphibian" 306 | }, 307 | { 308 | "name": "Cat", 309 | "diet": "Carnivore", 310 | "animalId": "Cat", 311 | "categoryId": "Mammal" 312 | }, 313 | { 314 | "diet": "Omnivore", 315 | "categoryId": "Bird", 316 | "animalId": "Sparrow", 317 | "name": "House sparrow" 318 | }, 319 | { 320 | "animalId": "Kangaroo", 321 | "name": "Red kangaroo", 322 | "diet": "Herbivore", 323 | "categoryId": "Mammal" 324 | }, 325 | { 326 | "diet": "Carnivore", 327 | "animalId": "Dog", 328 | "categoryId": "Mammal", 329 | "name": "Dog" 330 | }, 331 | { 332 | "diet": "Carnivore", 333 | "name": "Eagle", 334 | "animalId": "467D0044-4BB9-4C28-A10B-E87A2C328034", 335 | "categoryId": "Bird" 336 | } 337 | ] 338 | } 339 | } 340 | ] 341 | } 342 | ``` 343 | 344 | With a minimal amount of new code, we not only have an HTTP server running to deliver `Category` and `Animal` values, we also leverage our existing `LocalStore` for writing to a persistent database on our filesystem. 345 | 346 | [^1]: https://www.swift.org/documentation/server/ 347 | [^2]: https://www.swift.org/getting-started/vapor-web-server/ 348 | [^3]: https://github.com/apple/swift-async-algorithms 349 | -------------------------------------------------------------------------------- /Chapters/Chapter-16.md: -------------------------------------------------------------------------------- 1 | # AnimalsDataClient 2 | 3 | Our `AnimalsDataClient` executable was originally built to use `LocalStore` from command-line. Let’s try testing our `RemoteStore`. 4 | 5 | Select the `AnimalsData` package and open `Sources/AnimalsDataClient/main.swift`. Here is all we need for now: 6 | 7 | ```swift 8 | // main.swift 9 | 10 | import AnimalsData 11 | import Foundation 12 | import Services 13 | 14 | extension NetworkSession: RemoteStoreNetworkSession { 15 | 16 | } 17 | 18 | func makeRemoteStore() -> RemoteStore> { 19 | let session = NetworkSession(urlSession: URLSession.shared) 20 | return RemoteStore(session: session) 21 | } 22 | 23 | func main() async throws { 24 | let store = makeRemoteStore() 25 | 26 | let animals = try await store.fetchAnimalsQuery() 27 | print(animals) 28 | 29 | let categories = try await store.fetchCategoriesQuery() 30 | print(categories) 31 | } 32 | 33 | try await main() 34 | ``` 35 | 36 | If we start with running `AnimalsDataServer`, we can run `AnimalsDataClient` to perform queries and mutations on `RemoteStore`. 37 | 38 | Try it for yourself: experiment with mutating an `Animal` value on `RemoteStore`. Run your executables again and confirm the mutations were persisted. 39 | -------------------------------------------------------------------------------- /Chapters/Chapter-17.md: -------------------------------------------------------------------------------- 1 | # Animals.app 2 | 3 | We’re almost ready to launch our next-gen Animals product. All we have to do is make some changes on app launch. Select `Animals.xcodeproj` and open `AnimalsApp.swift`. 4 | 5 | Here is our new `AnimalsApp`: 6 | 7 | ```swift 8 | // AnimalsApp.swift 9 | 10 | import AnimalsData 11 | import AnimalsUI 12 | import ImmutableData 13 | import ImmutableUI 14 | import Services 15 | import SwiftUI 16 | 17 | @main @MainActor struct AnimalsApp { 18 | @State private var store = Store( 19 | initialState: AnimalsState(), 20 | reducer: AnimalsReducer.reduce 21 | ) 22 | @State private var listener = Listener(store: Self.makeRemoteStore()) 23 | 24 | init() { 25 | self.listener.listen(to: self.store) 26 | } 27 | } 28 | 29 | extension NetworkSession: @retroactive RemoteStoreNetworkSession { 30 | 31 | } 32 | 33 | extension AnimalsApp { 34 | private static func makeRemoteStore() -> RemoteStore> { 35 | let session = NetworkSession(urlSession: URLSession.shared) 36 | return RemoteStore(session: session) 37 | } 38 | } 39 | 40 | extension AnimalsApp: App { 41 | var body: some Scene { 42 | WindowGroup { 43 | Provider(self.store) { 44 | Content() 45 | } 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | If we start with running `AnimalsDataServer`, we can now build and run our application (`⌘ R`). Our Animals application will now use `RemoteStore` to save its state to our HTTP server. 52 | 53 | This is great! We just made a big leap in what this app is capable of: we added the ability to persist data to a remote server. Let’s also think about what we *didn’t* do: 54 | 55 | * Other than building `RemoteStore` and a few small changes to add `Codable` to data models, we didn’t have to change anything in our `AnimalsData` module. 56 | * We didn’t have to change anything in our `AnimalsUI` module. 57 | * We just changed a few lines of code in `AnimalsApp` to migrate from `LocalStore` to `RemoteStore`. 58 | 59 | To be fair, there are also some open questions we might have about scaling to more complex products that depend on fetching data from a remote server: 60 | 61 | * If we run our `AnimalsDataServer`, run our Animals application, then run our `AnimalsDataClient` to perform a mutation from command-line, our Animals application is now “stale”: we don’t display the current data. More complex products could introduce a solution similar to GraphQL subscriptions.[^1] If our Vapor server delivered a web-socket connection, we could use that for a “two-way” communication: when our source-of-truth is mutated remotely, we can then push the important changes back to our client. Relay has supported GraphQL subscriptions for many years to help solve this problem.[^2] 62 | * In a complex product, we might have multiple components that fetch similar data. An application might “over-fetch” too much data; this can reduce performance and drain battery life. If multiple components need the same data, we would like the option to make this fetch just one time. We would like more control over how and when data is cached. This is one of the problems that Relay was built to help solve.[^3] 63 | * Waiting for a network response can be slow. If our user performs an action — like deleting an `Animal` value — that should lead to a state mutation on our server, do we need to wait for our server to return? What if we just performed that state mutation directly on the client without waiting for our server to respond? These are known as “optimistic updates”, which we saw examples of when we built our Quakes product. Managing optimistic updates can be challenging: if our server fails to perform the mutation, we need some way to “roll back” the changes that happened locally. Relay manages a lot of this work and reduces the amount of code product engineers need to be responsible for.[^4] 64 | 65 | The Flux architecture was meant to be general and agnostic; Flux *could* have been used to build applications that fetched data from a remote server, but it also could have been used to build applications that saved all data locally. While React and Flux were being used internally at FB, product engineers began to solve the same problems over and over again shipping products that depended on a remote server. Writing repetitive code slows down product engineers and increases the chance of shipping bugs. Relay was built to solve these problems in just one place: the infra. 66 | 67 | Redux evolved from Flux, but independent of Relay. Redux and Relay solved different problems. Relay took the principles of Flux and shipped a complex framework for fetching data from a remote server. Redux took the principles of Flux and shipped a lightweight framework with stronger opinions about immutability. 68 | 69 | Like Flux and Redux, `ImmutableData` is lightweight. We built the infra ourselves in two chapters, and we saw that infra deploy to three different sample application products. Like Flux and Redux, `ImmutableData` is general and agnostic. We built three different sample application products with different needs: our first application saved data locally in-memory, our second application saved data in a persistent database, our third application fetched data from a remote server and saved data locally in a persistent database, and we migrated our second application to save data to a remote server. 70 | 71 | Unlike Relay, `ImmutableData` is not built with opinions about a remote server, or what kind of data that remote server would return. We built our Quakes product and our Animals product with remote data, but we wrote this code as a *product engineer*; this was product code, not infra code. 72 | 73 | In time, the `ImmutableData` architecture can continue evolving. A “next-gen” `ImmutableData` would probably look similar to Relay: the infra would ship with opinions about how a remote server works and how the data is structured. From those opinions, infra could then be written to solve for the repetitive problems that product engineers want solutions for: subscribing to remote changes, optimized caching logic, managing optimistic updates, and more. 74 | 75 | [^1]: https://graphql.org/learn/subscriptions/ 76 | [^2]: https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions/ 77 | [^3]: https://relay.dev/docs/guided-tour/reusing-cached-data/ 78 | [^4]: https://relay.dev/docs/tutorial/mutations-updates/#improving-the-ux-with-an-optimistic-updater 79 | -------------------------------------------------------------------------------- /Chapters/Chapter-18.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Let’s think back to the fundamental role Reducers play in Redux and `ImmutableData`: 4 | 5 | ```mermaid 6 | flowchart LR 7 | accTitle: Data Flow through Reducers 8 | accDescr: Our reducer maps from a State and an Action to a new State. 9 | oldState[State] --> Reducer 10 | Action --> Reducer 11 | Reducer --> newState[State] 12 | ``` 13 | 14 | A Reducer maps a State and an Action to a new State. The State and Action values are *immutable*; it’s not the job of a Reducer to attempt to *modify* the State in-place. The job of a Reducer is to return a *new* State value. 15 | 16 | Isn’t that slow? If the memory footprint of one State value is N bytes, does that imply that every time our Root Reducer runs we must copy all N bytes? 17 | 18 | Let’s go back and learn a little more about the evolution of Flux and Redux, the evolution of Swift, and how some specialized data structures can improve the performance of our Reducers. 19 | 20 | ## TreeDictionary 21 | 22 | The original Flux Architecture did not require Data Stores to contain only immutable data. Flux did not *require* mutable data models, but the common tutorials and examples were presented on mutable data models. An Action value would be dispatched to a Store, and a Store would perform an imperative mutation on a reference to a mutable data model: an Array or a Dictionary.[^1] Similar to Objective-C, the JavaScript standard library collections were reference types. 23 | 24 | One year after Flux was announced, Lee Byron introduced the ImmutableJS framework with some ambitious goals: adding immutable value semantics on JavaScript objects while *also* optimizing for performance.[^2] 25 | 26 | Conventional wisdom might tell you that if you want to copy a Dictionary of N elements *by value*, you must copy N elements. This is linear time: the amount of space and time to perform a copy scales linearly with the amount of elements in the Dictionary. If a Dictionary contains N elements, and you wish to add one new element while *also* preserving the original Dictionary, you must copy N elements to construct a new Dictionary. 27 | 28 | The insight of ImmutableJS was to use HAMT data structures as the “backing store” of a new Immutable Dictionary type.[^3] This type followed value semantics: immutability was enforced as the type was built. What HAMTs delivered was *fast* performance: structural sharing gave product engineers a `O(log n)` operation to perform copies. At scale, this was a big improvement over the `O(n)` operation to copy all N elements of our Dictionary. Because our Immutable Dictionary follows values semantics, these two values are now independent: adding one new element to our copy does not mutate our original. 29 | 30 | It was now possible to build Flux stores with Immutable Data without performing an `O(n)` operation on every Action that was dispatched to our Store. Redux took this one step further by *requiring* immutable data models in Stores.[^4] Immutable data improved predictability and testability: Reducers were pure functions without side effects. Immutable Data also gave Redux the opportunity to perform some performance optimizations: checking if two state values might have changed could now be performed with a reference equality check in constant time, as opposed to a value equality check in linear time. 31 | 32 | Swift ships with first-class support for value types: structs and enumerations. In languages that were primarily object-oriented, adding value semantics to your application often meant adding new code on top of the language itself: ImmutableJS brought value semantics to JavaScript collections and Remodel brought value semantics to Objective-C objects.[^5] In Swift, value semantics are provided by the language itself: we don’t need a library or framework. 33 | 34 | In a Swift Reducer, we can transform a `Dictionary` and return a new `Dictionary`. These are value types: the original `Dictionary` is unchanged. The `Dictionary` provided by the Swift standard library will perform a `O(n)` operation to copy its values. To preserve value semantics while *also* optimizing performance, we would like a data structure similar to ImmutableJS: an immutable Dictionary with a logarithmic operation to perform copies. 35 | 36 | The [`Swift-Collections`][^6] repo is maintained by Apple engineers, but ships outside the Swift standard library. The `TreeDictionary` data structure from `Swift-Collections` is built from CHAMP data structures.[^7] Like the HAMT data structures in ImmutableJS, the `TreeDictionary` data structure can perform copies in logarithmic time. Compared to linear time, this is a huge improvement when our State saves many values. 37 | 38 | We built two sample products that save a `Dictionary` value in State: Our Animals product saved a `Dictionary` of `Category` values and a `Dictionary` of `Animal` values, and our Quakes product saved a `Dictionary` of `Quake` values. For the most part, migrating to `TreeDictionary` is easy; it’s not a 100-percent drop-in replacement, but it’s pretty close. The basic APIs for reading and writing values remain the same. 39 | 40 | ## CowBox 41 | 42 | Let’s look a little deeper into the performance of `Dictionary`. Suppose we define a Reducer that performs an identity transform on State: this is a Reducer that returns its State parameter with no mutations. We might think this identity transformation is a copy operation: `O(n)` time. 43 | 44 | Suppose we then need to perform an equality check: we need to test that our copy is equal to our original value. We might think this is an `O(n)` operation: we test for value equality by iterating through all N values in our `Dictionary`. 45 | 46 | Swift Collections, including `Dictionary`, perform an important optimization: these are copy-on-write data structures. When we copy a collection value, we copy by reference. We share data between both copies. When we perform a mutation on a copy, we then copy by value. This implies that our Reducer that returns an identity transformation can return in constant time: to make a copy of a `Dictionary` containing N values, we only have to copy a pointer. To preserve value semantics, we “copy-on-write” before a mutation takes place; here is where the `O(n)` operation happens.[^8] 47 | 48 | Copy-on-write data structures can also improve performance when checking for value equality. If two copy-on-write data structures point to the same data reference, these copy-on-write data structures must be equal by value: we can return `true` in constant time. Redux, which required data to be modeled with immutable data, used a similar technique to optimize performance. Testing if two substates of our Redux state could have changed can use a reference equality check in constant time. When we build our `ImmutableData` state from copy-on-write data structures, we can take advantage of a similar optimization: checking if two substates could have changed can check for reference equality, which is constant time. 49 | 50 | Data structures like `Dictionary` and `TreeDictionary` implement copy-on-write, but the Swift language *itself* does not write that code for us. Engineers that need a copy-on-write data structure can write the code themselves to manage the object reference where data is stored.[^8] It’s not so bad, but it’s not so great, either. 51 | 52 | The `Swift-CowBox` repo makes it easy to add copy-on-write semantics to custom Swift structs.[^9] The `CowBox` macros attach to your struct declaration. All the boilerplate to manage the copy-on-write data storage is written for you at compile time by `CowBox`. This means we not only get faster copying of our custom data models, we also get faster checking for value equality. This can all add up to big performance wins when building SwiftUI applications.[^10] 53 | 54 | --- 55 | 56 | The `chapter-18` branch migrates `AnimalsState` and `QuakesState` to `TreeDictionary` and `CowBox`. Please reference this commit to see these data structures in action. 57 | 58 | Are these data structures the right choice for your own products? It depends. `TreeDictionary` and `CowBox` can have legit performance improvements when dealing with large amounts of complex data models, but there are performance tradeoffs. For small amounts of simple data models, these advanced data structures might not help improve performance: the overhead of the data structure itself might be more expensive than the performance improvements. 59 | 60 | If you plan to experiment with these data structures, our advice is to read the appropriate documentation to understand more about the performance tradeoffs. We also recommend measuring your performance with benchmarks. Measure CPU and memory before and after your migration to quantify how these data structures could be impactful in your own products. 61 | 62 | [^1]: https://www.youtube.com/watch?v=i__969noyAM 63 | [^2]: https://www.youtube.com/watch?v=I7IdS-PbEgI 64 | [^3]: https://en.wikipedia.org/wiki/Hash_array_mapped_trie 65 | [^4]: https://redux.js.org/faq/immutable-data 66 | [^5]: https://engineering.fb.com/2016/04/13/ios/building-and-managing-ios-model-objects-with-remodel/ 67 | [^6]: https://github.com/apple/swift-collections 68 | [^7]: https://github.com/apple/swift-collections/pull/31 69 | [^8]: https://www.youtube.com/watch?v=m9JZmP9E12M 70 | [^9]: https://github.com/Swift-CowBox/Swift-CowBox 71 | [^10]: https://github.com/Swift-CowBox/Swift-CowBox-Sample 72 | -------------------------------------------------------------------------------- /Chapters/Chapter-19.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | When we built our Quakes product, we saw how much faster the version built from `ImmutableData` was than the original version from Apple built on SwiftData. The original version not only performed expensive operations in SwiftData, those operations were blocking on `main`. Building from `ImmutableData` gave us a natural abstraction layer; the SwiftData operations to persist data on our filesystem could all take place asynchronously and concurrently without blocking `main`. 4 | 5 | Our Reducer is still a synchronous operation, and this operation still blocks `main`, but the immutable data structures we use to model our state are *very* lightweight compared to building our state in a SwiftData `ModelContext`. This is even taking into account that our Reducer returns *copies* of immutable data; our Reducer does not mutate our state value in-place. 6 | 7 | Let’s run some experiments to see for ourselves how efficient these immutable data structures are. We’re going to use two open-source repos for measuring benchmarks: [`CollectionsBenchmark`][^1] from Apple and [`Benchmarks`][^2] from Ordo One. If you’re not experienced with these repos, that’s ok. Running the benchmarks for yourself is optional. We’re going to focus this chapter on analyzing the results. 8 | 9 | If you want to follow along with the benchmarks to see for yourself where these measurements came from, you can checkout the [`ImmutableData-Benchmarks`](https://github.com/Swift-ImmutableData/ImmutableData-Benchmarks) repo. 10 | 11 | Our experiments will test three different data structure collections we could choose for the shared mutable state of our SwiftUI applications: 12 | 13 | * `ModelContext`: This is a reference type from SwiftData. Our data model elements are also reference types. 14 | * `Dictionary`: This is a value type from the Swift Standard Library. Our data model elements are also value types. 15 | * `TreeDictionary`: This is a value type from the [`Swift-Collections`][^3] repo. Our data model elements are also value types. 16 | 17 | Here is the data model element we use for testing `ModelContext`: 18 | 19 | ```swift 20 | @Model final class ModelElement : Hashable { 21 | var a: Int64 22 | var b: Int64 23 | var c: Int64 24 | var d: Int64 25 | var e: Int64 26 | var f: Int64 27 | var g: Int64 28 | var h: Int64 29 | var i: Int64 30 | var j: Int64 31 | } 32 | ``` 33 | 34 | Here is the data model element we use for testing `Dictionary` and `TreeDictionary`: 35 | 36 | ```swift 37 | struct StructElement : Hashable { 38 | var a: Int64 39 | var b: Int64 40 | var c: Int64 41 | var d: Int64 42 | var e: Int64 43 | var f: Int64 44 | var g: Int64 45 | var h: Int64 46 | var i: Int64 47 | var j: Int64 48 | } 49 | ``` 50 | 51 | Inspired by Jared Khan, these data models have a memory footprint of 80 bytes: ten times the width of one pointer on a 64-bit architecture.[^4] 52 | 53 | Now that we defined three different data structure collections and the data model elements in those collections, we can define the operations we want to measure on those collections. We begin by constructing a collection of N elements. We then perform the following operations on that collection: 54 | 55 | * We count the number of elements. 56 | * We read one existing element. 57 | * We insert one new element. 58 | * We update one existing element. 59 | * We delete one existing element. 60 | * We sort all N elements. 61 | 62 | We measure CPU and memory for every operation using `CollectionsBenchmark` and `Benchmarks`. To measure CPU, we run `CollectionsBenchmark` multiple times with increasing values of N. To measure memory, we run `Benchmarks` to measure the memory footprint when N is at our maximum. 63 | 64 | ## Count 65 | 66 | Let’s start by creating a collection of N elements and then measuring the performance of returning its element count. Here are our results from `CollectionsBenchmark`: 67 | 68 | 69 | 70 | 71 | 72 | `Dictionary` and `TreeDictionary` both return their element count in constant time: as the size of these collections grows, the time spent to count the elements stays about the same. This is great: this data structure scales well with large data. 73 | 74 | `ModelContext` needs more time to return its element count, and this time grows linearly with the size of the collection. By the time our collection is 64k elements, we are spending literal *orders of magnitude* more time waiting. 75 | 76 | Here is what CPU and memory look like from `Benchmarks` when our collection is 64k elements: 77 | 78 | ### Time (total CPU) 79 | 80 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 81 | |:----------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 82 | | Benchmarks:Collections.TreeDictionary: Count (μs) * | 2 | 3 | 3 | 3 | 3 | 7 | 7 | 100 | 83 | | Benchmarks:Swift.Dictionary: Count (μs) * | 3 | 3 | 4 | 4 | 4 | 8 | 8 | 100 | 84 | | Benchmarks:SwiftData.ModelContext: Count (μs) * | 18096 | 18235 | 18301 | 18399 | 18612 | 24969 | 25139 | 100 | 85 | 86 | ### Memory (resident peak) 87 | 88 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 89 | |:----------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 90 | | Benchmarks:Collections.TreeDictionary: Count (M) | 23 | 33 | 33 | 33 | 33 | 33 | 33 | 100 | 91 | | Benchmarks:Swift.Dictionary: Count (M) | 14 | 32 | 32 | 32 | 32 | 32 | 32 | 100 | 92 | | Benchmarks:SwiftData.ModelContext: Count (M) | 240 | 1050 | 1886 | 2722 | 3221 | 3513 | 3555 | 100 | 93 | 94 | The memory footprint of `Dictionary` and `TreeDictionary` is the same: about 32MB. The memory footprint of `ModelContext` is over an order of magnitude larger: the median memory footprint from our sample size of 100 is over 1800MB. 95 | 96 | ## Read 97 | 98 | Let’s create a collection of N elements and then measure the performance of reading one element. Here are our results from `CollectionsBenchmark`: 99 | 100 | 101 | 102 | 103 | 104 | `Dictionary` and `TreeDictionary` both return the element in constant time. There does look to be a little more “noise” from `TreeDictionary` at large values of N, but we don’t see a linear growth as N scales. 105 | 106 | `ModelContext` needs more time to return its element, and this time grows linearly with the size of the collection. Similar to our operation to count elements, we are spending orders of magnitude more time waiting at large values of N. 107 | 108 | Here is what CPU and memory look like from `Benchmarks` when our collection is 64k elements: 109 | 110 | ### Time (total CPU) 111 | 112 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 113 | |:---------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 114 | | Benchmarks:Collections.TreeDictionary: Read (μs) * | 2 | 3 | 3 | 3 | 3 | 6 | 6 | 100 | 115 | | Benchmarks:Swift.Dictionary: Read (μs) * | 3 | 3 | 4 | 4 | 4 | 10 | 11 | 100 | 116 | | Benchmarks:SwiftData.ModelContext: Read (μs) * | 20115 | 20234 | 20316 | 20464 | 20660 | 24150 | 27103 | 100 | 117 | 118 | ### Memory (resident peak) 119 | 120 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 121 | |:---------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 122 | | Benchmarks:Collections.TreeDictionary: Read (M) | 23 | 33 | 33 | 33 | 33 | 33 | 33 | 100 | 123 | | Benchmarks:Swift.Dictionary: Read (M) | 14 | 32 | 32 | 32 | 32 | 32 | 32 | 100 | 124 | | Benchmarks:SwiftData.ModelContext: Read (M) | 241 | 1055 | 1892 | 2720 | 3228 | 3519 | 3568 | 100 | 125 | 126 | This looks similar to our operation to count elements. Our total memory footprint for these collections remains about the same. 127 | 128 | ## Insert 129 | 130 | Let’s create a collection of N elements and then measure the performance of inserting one new element. Here are our results from `CollectionsBenchmark`: 131 | 132 | 133 | 134 | 135 | 136 | Here is where things start to get interesting. Let’s start with `Dictionary`. Our `Dictionary` is a value type. Our Reducer transforms our global state by returning a *new* immutable value. As discussed in our previous chapter, we can expect `Dictionary` to perform a linear amount of work when we perform a copy with a mutation applied: we “copy-on-write” all N elements to construct a new value. 137 | 138 | We see much better results from `TreeDictionary`. As discussed in our previous chapter, `TreeDictionary` is a value type that uses HAMT data structures. This means that performing a copy with a mutation applied performs `O(log n)` work. At small values of N, it does look like we pay a small performance penalty for `TreeDictionary`, but this overhead is worth it by the time our collection is 512 elements. By the time our collection is 64k elements, our `TreeDictionary` is performing its operation orders of magnitude faster than `Dictionary`. 139 | 140 | We measure two different operations for `ModelContext`: the operation to insert a new element *without* saving, and the operation to insert a new element *with* saving. The operation to insert without saving is constant time. At small values of N, this operation is orders of magnitude slower than `Dictionary` and `TreeDictionary`. At large values of N, it looks like the `O(n)` operation in `Dictionary` begins to catch up: `ModelContext` performs faster when our collection is 64k elements. `TreeDictionary` grows with N, but it grows logarithmically: at 64k elements, `TreeDictionary` is performing its operation orders of magnitude faster than `ModelContext`. 141 | 142 | Where `ModelContext` begins to slow down is when we insert a new element and then save our context. This becomes a `O(n)` operation starting at about 512 elements. By the time our collection is 64k elements, `ModelContext` is an order of magnitude slower than `Dictionary`. 143 | 144 | Here is what CPU and memory look like from `Benchmarks` when our collection is 64k elements: 145 | 146 | ### Time (total CPU) 147 | 148 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 149 | |:----------------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 150 | | Benchmarks:Collections.TreeDictionary: Insert (μs) * | 3 | 3 | 3 | 4 | 6 | 8 | 10 | 100 | 151 | | Benchmarks:Swift.Dictionary: Insert (μs) * | 891 | 928 | 936 | 947 | 961 | 988 | 993 | 100 | 152 | | Benchmarks:SwiftData.ModelContext: Insert (μs) * | 146 | 165 | 282 | 307 | 349 | 372 | 378 | 100 | 153 | | Benchmarks:SwiftData.ModelContext: Insert and Save (μs) * | 18317 | 18629 | 18727 | 18891 | 19284 | 25992 | 25992 | 100 | 154 | 155 | ### Memory (resident peak) 156 | 157 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 158 | |:----------------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 159 | | Benchmarks:Collections.TreeDictionary: Insert (M) | 23 | 30 | 30 | 30 | 30 | 30 | 30 | 100 | 160 | | Benchmarks:Swift.Dictionary: Insert (M) | 14 | 35 | 35 | 39 | 39 | 39 | 39 | 100 | 161 | | Benchmarks:SwiftData.ModelContext: Insert (M) | 254 | 1209 | 2202 | 3196 | 3794 | 4148 | 4185 | 100 | 162 | | Benchmarks:SwiftData.ModelContext: Insert and Save (M) | 241 | 1044 | 1879 | 2716 | 3215 | 3525 | 3558 | 100 | 163 | 164 | Our `TreeDictionary` uses HAMT data structures and structural sharing. In addition to saving time, this also saves memory compared to a data structure like `Dictionary` that copies all N values when returning a new copy with a mutation applied. 165 | 166 | ## Update 167 | 168 | Let’s create a collection of N elements and then measure the performance of updating one existing element. Here are our results from `CollectionsBenchmark`: 169 | 170 | 171 | 172 | 173 | 174 | This looks similar to what we saw in our previous operation, except that updating an existing element in `ModelContext` is a `O(n)` operation *before* the save operation takes place. `Dictionary` is still a `O(n)` operation. `TreeDictionary` still performs best at large values of N. 175 | 176 | Here is what CPU and memory look like from `Benchmarks` when our collection is 64k elements: 177 | 178 | ### Time (total CPU) 179 | 180 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 181 | |:----------------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 182 | | Benchmarks:Collections.TreeDictionary: Update (μs) * | 4 | 4 | 4 | 5 | 7 | 9 | 10 | 100 | 183 | | Benchmarks:Swift.Dictionary: Update (μs) * | 912 | 942 | 961 | 975 | 998 | 1141 | 1236 | 100 | 184 | | Benchmarks:SwiftData.ModelContext: Update (μs) * | 20081 | 20349 | 20431 | 20595 | 20922 | 27410 | 27523 | 100 | 185 | | Benchmarks:SwiftData.ModelContext: Update and Save (μs) * | 20481 | 20660 | 20742 | 20873 | 21119 | 27820 | 28453 | 100 | 186 | 187 | ### Memory (resident peak) 188 | 189 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 190 | |:----------------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 191 | | Benchmarks:Collections.TreeDictionary: Update (M) | 23 | 33 | 33 | 33 | 33 | 33 | 33 | 100 | 192 | | Benchmarks:Swift.Dictionary: Update (M) | 14 | 43 | 43 | 43 | 43 | 43 | 43 | 100 | 193 | | Benchmarks:SwiftData.ModelContext: Update (M) | 252 | 1217 | 2210 | 3213 | 3802 | 4163 | 4199 | 100 | 194 | | Benchmarks:SwiftData.ModelContext: Update and Save (M) | 249 | 1043 | 1888 | 2716 | 3223 | 3515 | 3559 | 100 | 195 | 196 | The memory usage looks similar to our previous results. 197 | 198 | ## Delete 199 | 200 | Let’s create a collection of N elements and then measure the performance of deleting one existing element. Here are our results from `CollectionsBenchmark`: 201 | 202 | 203 | 204 | 205 | 206 | This looks similar to what we saw in our previous operation: `ModelContext` and `Dictionary` both grow linearly and `TreeDictionary` performs best at large values of N. 207 | 208 | Here is what CPU and memory look like from `Benchmarks` when our collection is 64k elements: 209 | 210 | ### Time (total CPU) 211 | 212 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 213 | |:----------------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 214 | | Benchmarks:Collections.TreeDictionary: Delete (μs) * | 4 | 4 | 4 | 5 | 7 | 10 | 19 | 100 | 215 | | Benchmarks:Swift.Dictionary: Delete (μs) * | 908 | 923 | 935 | 947 | 960 | 1000 | 1130 | 100 | 216 | | Benchmarks:SwiftData.ModelContext: Delete (μs) * | 19293 | 20283 | 20349 | 20464 | 20726 | 27640 | 28067 | 100 | 217 | | Benchmarks:SwiftData.ModelContext: Delete and Save (μs) * | 16519 | 20775 | 20873 | 21021 | 21299 | 27460 | 27549 | 100 | 218 | 219 | ### Memory (resident peak) 220 | 221 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 222 | |:----------------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 223 | | Benchmarks:Collections.TreeDictionary: Delete (M) | 23 | 32 | 33 | 33 | 33 | 33 | 33 | 100 | 224 | | Benchmarks:Swift.Dictionary: Delete (M) | 14 | 36 | 40 | 40 | 40 | 40 | 40 | 100 | 225 | | Benchmarks:SwiftData.ModelContext: Delete (M) | 254 | 1218 | 2212 | 3204 | 3804 | 4159 | 4201 | 100 | 226 | | Benchmarks:SwiftData.ModelContext: Delete and Save (M) | 248 | 1069 | 1914 | 2741 | 3238 | 3542 | 3571 | 100 | 227 | 228 | The memory usage looks similar to our previous results. 229 | 230 | ## Sort 231 | 232 | Let’s create a collection of N elements and then measure the performance of sorting all elements. Here are our results from `CollectionsBenchmark`: 233 | 234 | 235 | 236 | 237 | 238 | Sorting is an `O(n log n)` operation. We expect this to grow as the size of our collection grows. We do seem to notice that sorting `Dictionary` and `TreeDictionary` values seems to return an order of magnitude faster than performing a sorted fetch on a `ModelContext`. 239 | 240 | Here is what CPU and memory look like from `Benchmarks` when our collection is 64k elements: 241 | 242 | ### Time (total CPU) 243 | 244 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 245 | |:---------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 246 | | Benchmarks:Collections.TreeDictionary: Sort (μs) * | 335467 | 336331 | 336331 | 336855 | 337117 | 337904 | 339169 | 100 | 247 | | Benchmarks:Swift.Dictionary: Sort (μs) * | 326249 | 327680 | 328204 | 328729 | 328991 | 329777 | 330451 | 100 | 248 | | Benchmarks:SwiftData.ModelContext: Sort (μs) * | 2229116 | 2254438 | 2267021 | 2271216 | 2279604 | 2321547 | 2338431 | 100 | 249 | 250 | ### Memory (resident peak) 251 | 252 | | Test | p0 | p25 | p50 | p75 | p90 | p99 | p100 | Samples | 253 | |:---------------------------------------------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:| 254 | | Benchmarks:Collections.TreeDictionary: Sort (M) | 27 | 36 | 36 | 36 | 37 | 37 | 37 | 100 | 255 | | Benchmarks:Swift.Dictionary: Sort (M) | 34 | 34 | 34 | 34 | 34 | 34 | 34 | 100 | 256 | | Benchmarks:SwiftData.ModelContext: Sort (M) | 336 | 1165 | 2015 | 2844 | 3341 | 3645 | 3672 | 100 | 257 | 258 | It’s difficult to make a strong judgement about memory usage of `ModelContext` when sorting compared to memory usage of `ModelContext` when inserting or updating. If you wanted to measure more closely, you could experiment with increasing the sample size to see if you can control for any noise in our measurements. What we’re most concerned about is that our immutable data structures — `Dictionary` and `TreeDictionary` — consume orders of magnitude less memory than `ModelContext`. 259 | 260 | --- 261 | 262 | Our original and primary goal when choosing `ImmutableData` instead of SwiftData was semantics. Building SwiftUI directly on SwiftData means our mental model for managing state is imperative and our data models are mutable. Building SwiftUI directly on `ImmutableData` means our mental model for managing state is declarative and our data models are immutable. 263 | 264 | If `ImmutableData` gave us value semantics and a declarative programming model, the argument could be made we would prefer `ImmutableData` *even if* SwiftData offered better performance. If `ImmutableData` gives us values semantics, a declarative programming model, *and* better performance than SwiftData, we can strongly recommend this architecture for your own products and teams. 265 | 266 | [^1]: https://github.com/apple/swift-collections-benchmark 267 | [^2]: https://github.com/ordo-one/package-benchmark 268 | [^3]: https://github.com/apple/swift-collections 269 | [^4]: https://jaredkhan.com/blog/swift-copy-on-write#many-structs 270 | -------------------------------------------------------------------------------- /Chapters/Chapter-20.md: -------------------------------------------------------------------------------- 1 | # Next Steps 2 | 3 | As they say at FB: *This journey is one-percent finished.* 4 | 5 | We covered a lot of ground in this tutorial. We built a new infra library from scratch, we built multiple sample application products, we learned about some external dependencies we can import for specialized data structures to improve performance, and we ran benchmarks to measure how these immutable data structures compare to SwiftData. 6 | 7 | If you’re ready to experiment with the `ImmutableData` architecture in your own products, you have a few different options available: 8 | 9 | * If you have the ability to import a new external dependency in your product, you can import the [`ImmutableData`](https://github.com/Swift-ImmutableData/ImmutableData) repo package. This is a “standalone” package version of the infra we built in this Programming Guide. 10 | * If you have a product that deploys to older operating systems, you might be blocked on importing `ImmutableData` because of the dependencies on `Observable` and variadic types. The [`ImmutableData-Legacy`](https://github.com/Swift-ImmutableData/ImmutableData-Legacy) repo package is a version of the `ImmutableData` infra that deploys to older operating systems. 11 | * If you are blocked on importing *any* new external dependency, you can follow the steps in this Programming Guide to build your own version of the `ImmutableData` architecture from scratch. 12 | 13 | If you are building a new product from scratch, the sample application products built in this Programming Guide can help you begin to think creatively how `ImmutableData` can be used for your own product domain. 14 | 15 | If you are attempting to bring `ImmutableData` to an *existing* product built on a legacy architecture, we recommend reading our [`ImmutableData-FoodTruck`](https://github.com/Swift-ImmutableData/ImmutableData-FoodTruck) tutorial. The `ImmutableData-FoodTruck` tutorial is an *incremental* migration: we show you how the `ImmutableData` architecture can “coexist” along with a legacy architecture built on imperative logic and mutable data models. 16 | 17 | As always, please file a new GitHub issue if you encounter any compatibility problems, issues, or bugs with `ImmutableData`. Please let us know if there are any missing pieces: anything that is blocking you or your team from migrating to `ImmutableData`. 18 | 19 | Thanks! 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The ImmutableData Programming Guide 2 | 3 | “What is the best design pattern for SwiftUI apps?” 4 | 5 | We hear this question a lot. Compared to the days when AppKit and UIKit were the dominant frameworks for product engineering in the Apple Ecosystem, Apple has been relatively un-opinionated about what kind of design pattern engineers should choose “by default” for their SwiftUI applications. 6 | 7 | Many engineers in the SwiftUI community are currently evangelizing a “MVVM” design pattern. Other engineers are making the argument that SwiftUI is really encouraging a “MVC” design pattern. You might have also heard discussion of a “MV” design pattern. These design patterns share a fundamental philosophy: the state of your application is managed from your view components using imperative logic on mutable model objects. To put it another way, these design patterns start with a fundamental assumption of *mutability* that drives the programming model that product engineers must opt-in to when building graphs of view components. The “modern and declarative” programming model product engineers have transitioned to for SwiftUI is then paired with a “legacy and imperative” programming model for managing shared mutable state. 8 | 9 | Over the course of this project, we present what we think is a better way. Drawing on over a decade of experience shipping products at scale using declarative UI frameworks, we present a new application architecture for SwiftUI. Using the Flux and Redux architectures as a philosophical prior art, we can design an architecture using modern Swift and specialized for modern SwiftUI. This architecture encourages *declarative* thinking instead of *imperative* thinking, *functional* programming instead of object-oriented programming, and *immutable* model values instead of mutable model objects. 10 | 11 | At the core of the architecture is a unidirectional data flow: 12 | 13 | ```mermaid 14 | flowchart LR 15 | accTitle: Data Flow in the ImmutableData Framework 16 | accDescr: Data flows from action to state, and from state to view, in one direction only. 17 | A[Action] --> B[State] --> C[View] 18 | ``` 19 | 20 | All global state data flows through the application following this basic pattern, and a strict separation of concerns is enforced. The actions *declare* what has occurred, whether user input, a server response, or a change in a device’s sensors, but they have no knowledge of the state or view layers. The state layer *reacts* to the “news” described by the action and updates the state accordingly. All logic for making changes to the state is contained within the state layer, but it knows nothing of the view layer. The views then *react* to the changes in the state layer as the new state flows through the component tree. Again, however, the view layer knows nothing about the state layer. By maintaining this strict unidirectional data flow and separation of concerns, our application code becomes easier to test, easier to reason about, easier to explain to new team members, and easier to update when new features are required. 21 | 22 | Further, avoiding complexity like two-way data bindings, or the spaghetti engendered by mutability, allows our code to become clean, fast, and maintainable. This is the key difference between this application framework (and the ideas behind it) and other presentations of actions, state, and view previously shown at WWDC.[^1] By avoiding direct mutations called from outside the state layer and embracing immutability instead, complexity vanishes and our code becomes much more robust. 23 | 24 | We call this framework and architecture `ImmutableData`. We present `ImmutableData` as a free and open-source project with free and open-source documentation. Over the course of this tutorial, we will show you, step-by-step, how the `ImmutableData` infra is built. Once the infra is ready, we will then build, step-by-step, multiple sample applications using SwiftUI to display and transform state through the `ImmutableData` architecture. 25 | 26 | ## Requirements 27 | 28 | Our goal is to teach a new way of thinking about state management and data flow for SwiftUI. Our goal *is not* to teach Swift Programming or the basics of SwiftUI. You should have a strong competency in Swift 6.0 before beginning this tutorial. You should also have a working familiarity with SwiftUI. A working familiarity with SwiftData would be helpful, but is not required. 29 | 30 | Inspired by Matt Gallagher, our project will make heavy use of modules and access control to keep our code organized.[^2] A working familiarity with Swift Package Manager will be helpful, but our use of Swift Package APIs will be kept at a relatively basic level. 31 | 32 | The `ImmutableData` infra deploys to the following platforms: 33 | * iOS 17.0+ 34 | * iPadOS 17.0+ 35 | * macOS 14.0+ 36 | * tvOS 17.0+ 37 | * visionOS 1.0+ 38 | * watchOS 10.0+ 39 | 40 | Building the `ImmutableData` tutorial requires Xcode 16.0+ and macOS 14.5+. 41 | 42 | Please file a GitHub issue if you encounter any compatibility problems. 43 | 44 | ## Organization 45 | 46 | *The ImmutableData Programming Guide* is inspired by “long-form” documentation like [*Programming with Objective-C*][^3] and [*The Swift Programming Language*][^4]. 47 | 48 | This guide includes the following chapters: 49 | 50 | ### Part 0: Overview 51 | * [Chapter 00](Chapters/Chapter-00.md): We discuss the history and evolution of Flux, Redux, and SwiftUI. In what ways did SwiftUI evolve in a similar direction as React? How can our `ImmutableData` architecture use ideas from React to improve product engineering for SwiftUI? 52 | ### Part 1: Infra 53 | * [Chapter 01](Chapters/Chapter-01.md): We build the `ImmutableData` module for managing the global state of our application. 54 | * [Chapter 02](Chapters/Chapter-02.md): We build the `ImmutableUI` module for making our global state available to SwiftUI view components. 55 | ### Part 2: Products 56 | * [Chapter 03](Chapters/Chapter-03.md): We build the data models of our Counter application: a simple SwiftUI app to increment and decrement an integer. 57 | * [Chapter 04](Chapters/Chapter-04.md): We build the component tree of our Counter application. 58 | * [Chapter 05](Chapters/Chapter-05.md): We build and run our Counter application. 59 | * [Chapter 06](Chapters/Chapter-06.md): We build the data models of our Animals application: a SwiftUI app to store a collection of data models with persistence to a local database. 60 | * [Chapter 07](Chapters/Chapter-07.md): We build a command-line utility for testing the data models of our Animals application without any component tree. 61 | * [Chapter 08](Chapters/Chapter-08.md): We build the component tree of our Animals application. 62 | * [Chapter 09](Chapters/Chapter-09.md): We build and run our Animals application. 63 | * [Chapter 10](Chapters/Chapter-10.md): We build the data models of our Quakes application: a SwiftUI app to fetch a collection of data models from a remote server with persistence to a local database. 64 | * [Chapter 11](Chapters/Chapter-11.md): We build a command-line utility for testing the data models of our Quakes application without any component tree. 65 | * [Chapter 12](Chapters/Chapter-12.md): We build the component tree of our Quakes application. 66 | * [Chapter 13](Chapters/Chapter-13.md): We build and run our Quakes application. 67 | * [Chapter 14](Chapters/Chapter-14.md): We update the data models of our Animals application to support persistence to a remote server. 68 | * [Chapter 15](Chapters/Chapter-15.md): We build an HTTP server for testing our new Animals application. 69 | * [Chapter 16](Chapters/Chapter-16.md): We build a command-line utility for testing the data models of our new Animals application without any component tree. 70 | * [Chapter 17](Chapters/Chapter-17.md): We build and run our new Animals application. 71 | ### Part 3: Performance 72 | * [Chapter 18](Chapters/Chapter-18.md): We learn about specialized data structures that can improve the performance of our applications when working with large amounts of data that is copied many times. 73 | * [Chapter 19](Chapters/Chapter-19.md): We run benchmarks to measure how the performance of immutable collection values compare to SwiftData. 74 | ### Part 4: Next Steps 75 | * [Chapter 20](Chapters/Chapter-20.md): Here are some final thoughts about what’s coming next. 76 | 77 | ## Companion Repos 78 | 79 | You can find more repos on our `ImmutableData` GitHub organization: 80 | 81 | * [`ImmutableData-Samples`](https://github.com/Swift-ImmutableData/ImmutableData-Samples) includes empty Swift packages, empty Xcode projects, and an empty Xcode workspace. This is the recommended way to complete our tutorial. The workspace provides some basic setup (like adding dependencies between packages) that will let you focus on our tutorial. 82 | * [`ImmutableData-Benchmarks`](https://github.com/Swift-ImmutableData/ImmutableData-Benchmarks) includes benchmarks to measure performance. These benchmarks will be discussed in Chapter 19. 83 | * [`ImmutableData`](https://github.com/Swift-ImmutableData/ImmutableData) is the “standalone” repo package of the `ImmutableData` infra. This is for product engineers that want to use the infra in their products without building the infra from scratch. 84 | * [`ImmutableData-Legacy`](https://github.com/Swift-ImmutableData/ImmutableData-Legacy) is a version of the `ImmutableData` infra that deploys to older operating systems. 85 | * [`ImmutableData-FoodTruck`](https://github.com/Swift-ImmutableData/ImmutableData-FoodTruck) is an incremental migration tutorial. We begin with an app from Apple built on a legacy architecture. We then show how the `ImmutableData` infra can be used to incrementally migrate individual components and product surfaces *without* changing the behavior of components and product surfaces built on the legacy infra. We show how `ImmutableData` can “coexist” with a legacy architecture. 86 | 87 | ## License 88 | 89 | Copyright 2024 Rick van Voorden and Bill Fisher 90 | 91 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 92 | 93 | http://www.apache.org/licenses/LICENSE-2.0 94 | 95 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 96 | 97 | [^1]: https://developer.apple.com/videos/play/wwdc2019/226 98 | [^2]: https://www.cocoawithlove.com/blog/app-submodules.html 99 | [^3]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/ 100 | [^4]: https://www.swift.org/documentation/tspl/ 101 | --------------------------------------------------------------------------------