ShabanKamell / StockWise

StockWise is a powerful app provides everything you need to track and monitor stocks, all in one convenient location. With user-friendly features and real-time data, StockWise is the perfect solution for anyone looking to stay ahead of the game. With StockWise, you'll have access to up-to-date stock prices, news, and trends, all in real-time.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

StockWise

StockWise is a powerful app provides everything you need to track and monitor stocks, all in one convenient location. With user-friendly features and real-time data, StockWise is the perfect solution for anyone looking to stay ahead of the game.

With StockWise, you'll have access to up-to-date stock prices, news, and trends, all in real-time. Our app provides detailed charts and graphs, so you can easily track the performance of your favorite stocks and make informed decisions.

Important Note

If you come across the "Missing Libs framework" error during the initial build of the application, simply drag and drop the Libs folder from the root directory into the files section of Xcode.

Table of contents

Technologies & Frameworks

  • Flutter
  • Dart
  • Swift
  • SwiftUI
  • Combine
  • Async/Await
  • Moya
  • MVVM
  • Repository and data sources patterns
  • Modular Architecture

Flutter

The user interface (UI) layer is developed using Flutter and integrated into the native app. Meanwhile, the data layer is implemented in the native app. Communication between Flutter and the host is managed through the utilization of the Pigeon library.

Managing Communication Between Flutter and the Host

I decided to using the Pigeon package for communication between Flutter and the host platform because traditional platform channels had many issues and were difficult to maintain and scale. Pigeon is a Flutter package that generates type-safe communication code, providing a simpler and more efficient way of communicating between Flutter and the host platform. The code generated by Pigeon is strongly typed, reducing errors and improving code maintainability. Additionally, Pigeon allows for the creation of custom data types that can be shared between Flutter and the host platform, making it easier to transfer complex data structures. Unlike traditional platform channels that require manual implementation of serialization and deserialization logic, Pigeon handles this automatically, saving time and effort while reducing the likelihood of bugs or errors in the communication code. Overall, Pigeon is a powerful tool that simplifies communication between Flutter and the host platform, making it easier to develop high-quality cross-platform apps.

Architecture

Image

There are a lot of trade-offs to implement the app architecture. But we chose to go with The Clean Architecture for the following reasons:

The Dependency Rule: The most fundamental principle of clean architecture is the Dependency Rule, which states that the dependencies between modules should flow inward, toward higher-level policy modules, and not outward, toward lower-level implementation details. This allows for loose coupling between modules, which makes the code more modular and easier to maintain.

The Architecture: The architecture of a clean system is divided into four layers: the Entities layer, which contains the business objects; the Use Cases layer, which contains the application-specific business rules; the Interface Adapters layer, which contains the adapters that convert data from the external world to the internal world and vice versa; and the Frameworks and Drivers layer, which contains the external tools and libraries that the application uses.

The SOLID Principles: The SOLID principles are a set of principles for writing clean, maintainable code. They stand for Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. These principles provide guidance on how to write code that is modular, testable, and easy to maintain.

The Clean Code: The code in a clean architecture should be clean, readable, and easy to understand. This means following good coding practices such as using meaningful variable and function names, writing clear and concise comments, and avoiding duplication and unnecessary complexity.

Testing: Testing is an important part of clean architecture. The code should be designed in a way that makes it easy to write unit tests, and automated tests should be run frequently to ensure that the code behaves as expected.

App Modules

Our app is designed with a modular architecture, consisting of four main modules: Screens, Core, Data, and Presentation. Each module is completely independent of the others, and can be added as a framework for increased reusability and modularity.

The Screens module contains all the screens of the app, while the Presentation module includes all shared and reusable UI components that can be used throughout the app. The Data module contains all files related to data, such as network, user defaults, caching, and other data files. Finally, the Core module contains all code that is not related to Presentation or Data, such as business logic, algorithms, and other core functionalities.

To illustrate the benefits of this modular architecture, we have implemented a similar approach in another project, where each module is available as a separate framework. This approach enables us to easily maintain and update each module individually, without affecting the rest of the app. It also allows us to reuse modules in other projects, resulting in faster build, development and improved overall code quality. To gain a better understanding of the modular architecture of our app, we invite you to take a look at this project, https://github.com/ShabanKamell/Contacts-SwiftUI.

App Layers

Image

The app has a well-designed architecture that follows the principles of separation of concerns, which allows for maintainability, scalability, and testability. It is divided into multiple layers, each with a specific responsibility:

Presentation Layer This layer is responsible for displaying the user interface and handling user interactions. It includes UI components such as SwiftUI views.

ViewModel Layer This layer acts as an intermediary between the presentation layer and the data layer. It contains the business logic of the app and manages the state of the UI. It also communicates with the repository layer to retrieve and update data.

Repository Layer This layer is responsible for providing data to the ViewModel. It abstracts the data source layer and provides a clean, consistent interface for accessing data. It also handles caching and local storage.

Data Source Layer This layer is responsible for communicating with external data sources such as a database or web service. It provides an interface for the repository to retrieve data.

Network Layer This layer is responsible for handling network requests and responses. It's communicated with the data source layer to retrieve data from external APIs.

Caching Layer This layer serves the purpose of caching data to facilitate offline usage and managing expired objects to ensure efficient memory utilization.

User Defaults Layer This layer is responsible for storing small amounts of data such as user preferences and settings. It provides a simple interface for storing and retrieving data.

By separating the app into these layers, it is easier to maintain, test, and scale the app over time.

MVVM, Repository, and Data Sources

MVVM, Repository pattern, and Data Sources are 3 essential concepts that can be used together to build modern and maintainable iOS applications.

MVVM is an architectural design pattern that separates an application's concerns into three distinct layers: Model, View, and ViewModel. The Model represents the data and business logic of the application, the View is responsible for displaying the user interface, and the ViewModel acts as an intermediary between the Model and the View, providing data to the View and handling user input from the View.

The Repository pattern is a design pattern that provides a layer of abstraction between the application and the data persistence layer. It helps to isolate the data access logic from the rest of the application, making it easier to maintain and test.

For example, the SwiftUI View might use Combine to subscribe to a data stream from the ViewModel. The ** Repository** is responsible for fetching the data from a remote API or a local database and providing it to the ViewModel. The ** ViewModel** can then subscribe to this publisher and update its own properties accordingly.

Overall, using MVVM, Repository pattern, and Data Sources together can help to create a clear separation of concerns in the application, making it easier to maintain and test. It can also provide a more reactive and declarative approach to handling asynchronous data streams and user input events, which can lead to more concise and readable code.

MVVM Abstractions

When implementing MVVM, it's important to abstract the basic components of the architecture to improve code reusability and maintainability. One way to do this is by creating abstractions for the view, view model, and asynchronous operations.

The AppScreen can be thought of as an abstraction for the view component in MVVM. It encapsulates the logic and behavior of the view, making it easier to modify and reuse across different screens and components.

Similarly, the AppViewModel is an abstraction for the ViewModel component in MVVM. It acts as an intermediary between the view and the model, handling user input and updating the view based on changes in the model.

Finally, the AsyncMan abstraction is used to manage asynchronous operations in the application. It encapsulates the logic for handling Async/Await calls, making it easier to manage and debug these operations throughout the codebase.

Overall, by abstracting these basic components in MVVM, you can create a more modular and maintainable application with improved code quality and reusability.

Reusable Components

To ensure that our application is both scalable and maintainable, it is important to follow the "Don't Repeat Yourself" (DRY) principle and maximize code reuse wherever possible.

One way to achieve this is by using reusable components throughout the application. For instance, we use AppTextField as a component that can be reused in different parts of the application wherever a text field is needed. Similarly, we can use PlainList component that can be used in place of a regular list wherever appropriate.

By using such reusable components, we can not only reduce the amount of code we need to write but also make it easier to maintain and update the application in the future. Additionally, it can also improve the consistency and coherence of the application, as the same components are used throughout.

Threading

The implementation of threading in our project utilizes Async/Await, which is an excellent addition from Apple. Using this approach makes our code more concise, readable, and maintainable. By leveraging the power of Async/Await, we can easily write asynchronous code that is easier to understand and debug. This approach also helps us avoid complex and error-prone code that can occur with traditional threading approaches. Overall, Async/Await provides a streamlined and efficient way to handle concurrency in our project, helping us to deliver a high-quality and reliable product.

Asynchronous Operations Handling

In the Threading section discussed earlier, the project utilizes the powerful Async/Await feature to handle asynchronous operations. To streamline the management of these operations throughout the codebase, the project also employs an abstraction called AsyncMan. This abstraction encapsulates the necessary logic for handling Async/Await calls, which simplifies debugging and management of the asynchronous operations. By using this approach, the codebase becomes more organized and maintainable, while also reducing the potential for errors and improving overall code quality.

Error Handling

To effectively handle errors that occur during asynchronous operations, the project implements a strategy pattern using the ErrorHandler protocol. This protocol can be adopted by any struct or class to add error handling for specific types of errors. By leveraging the ErrorHandler protocol, the project can handle errors more efficiently and effectively, reducing the potential for disruptions to user experience and improving overall app stability.

For instance, the UnauthorizedErrorHandler is an example of an error handler that addresses token expiration issues. When a token expires, this error handler refreshes the token and retries the failed request, preventing the need for the user to manually log back in.

Network

The network layer in our app is implemented using the Moya library, which provides a simplified interface for networking with Alamofire. Moya abstracts away the complexities of Alamofire and makes it easier to define and consume APIs using enums. With Moya, we can define our API endpoints as a set of cases in an enum, making our networking code more readable, maintainable, and testable. This approach also helps to decouple our networking layer from our business logic, making it easier to make changes to our network requests without affecting the rest of our app.

Resources

One significant limitation of iOS resources is that they rely on String identifiers to reference them, which can lead to bugs when these identifiers are changed without updating all references. To mitigate this issue, I'm using SwiftGen to generate strongly-typed classes for the resources. This approach allows us to refer to resources using code rather than strings, which can improve both readability and maintainability of our code.

The SwiftGen code is located in /etc/swiftgen and in the project's build phase, it's invoked through the script /etc/scripts/CommonTargetScripts.sh. When SwiftGen runs, it generates several classes, including Assets.swift, Strings.swift, Colors.swift, and Fonts.swift. These generated classes are typically placed in the App/resources folder of the project.

Caching

Although there are various options available for implementing offline caching, such as NSCache, CoreData, and Realm, we have decided to use a cache library. While each approach has its own trade-offs, we believe that the Cache library will be the most suitable solution for my particular use case. The Cache library is a powerful caching solution that provides many benefits, including:

  • Compatibility with Swift Codable, making it easy to save and load any object that conforms to the Codable protocol using the Storage feature.
  • Hybrid storage with both memory and disk options, allowing for efficient use of resources and fast access to
  • cached data.
  • Customizable options for disk and memory storage through the DiskConfig and MemoryConfig classes.
  • Built-in support for object expiry and cleanup, ensuring that expired objects are automatically removed from the cache.
  • Thread safety, allowing for concurrent access to the cache from multiple queues or threads.
  • Synchronous access by default, with support for asynchronous APIs for more complex use cases.
  • Extensive unit testing and documentation, ensuring that the library is reliable and easy to use.

Caching Strategy

Our app adopts an offline-first approach, which means that we prioritize serving data from the local cache. When a user requests data, we first check if it's available in the cache. If the data is present, we retrieve and display it immediately to ensure a smooth user experience. If the data is not available in the cache, we fetch it from the remote server and update the cache accordingly. Once the cache is updated, we reflect the changes on the user interface, ensuring that the user always sees the latest data.

The cache is set to expire after 30 minutes and is subsequently deleted when a request for the cache is made at any time.

Swift Package Manager Dependencies

The Swift Package Manager is the preferred dependency manager for the app. To manage dependencies, we have chosen to use a Package.swift file instead of relying on Xcode's default approach. This decision was made for several reasons. Firstly, adding packages with Package.swift is straightforward and intuitive. Secondly, having all packages listed in a single file makes it easier to track changes to dependencies in the Git history. Additionally, using Package.swift allows for greater flexibility in managing dependencies outside of Xcode. Overall, using the Swift Package Manager with a Package.swift file has proven to be a reliable and efficient approach for managing dependencies in my app.

Third-Party Dependencies Integration

The third-Party dependencies can introduce complexity into our codebase. If they are not isolated properly, they can make our code more difficult to maintain and upgrade.

To isolate third-party libraries, we use design patterns like the Adapter or Facade pattern. These patterns allow to create a layer of abstraction between the app code and the third-party library, so that the app only interacts with a simplified interface. This can make it easier to change or replace the underlying library without affecting the rest of your code.

As an example, I have implemented an interface for reporting messages to the user in our app. The Reportable protocol defines the contract for reporting messages, and it depends on the SwiftMessages library for displaying the messages. By using this interface, we can easily swap out the SwiftMessages library with another library that provides the same functionality, without affecting the app code. This is because the interface remains the same, and only the underlying implementation needs to be changed.

Dependencies

SwiftUINavigator (My own library)

SwiftUINavigator is an on-the-fly approach for handling navigation in SwiftUI. It provides a familiar way of handling navigation similar to UIKit, where you can push or present a view controller without the need to declare links or local state variables. This approach is more flexible and allows for dynamic navigation, making it easier to build more complex navigation flows in your SwiftUI app. Unlike traditional navigation patterns in SwiftUI, SwiftUINavigator offers a more intuitive and straightforward way of managing your app's navigation hierarchy.

SwiftMessages

SwiftMessages is a very flexible view and view controller presentation library for iOS. Message views and view controllers can be displayed at the top, bottom, or center of the screen, or behind navigation bars and tab bars. There are interactive dismiss gestures including a fun, physics-based one. Multiple background dimming modes. And a lot more!

Moya

You're a smart developer. You probably use Alamofire to abstract away access to URLSession and all those nasty details you don't really care about. But then, like lots of smart developers, you write ad hoc network abstraction layers. They are probably called "APIManager" or "NetworkModel", and they always end in tears.

Cache

Cache doesn't claim to be unique in this area, but it's not another monster library that gives you a god's power. It does nothing but caching, but it does it well. It offers a good public API with out-of-box implementations and great customization possibilities. Cache utilizes Codable in Swift 4 to perform serialization.

SwiftGen

SwiftGen is a tool to automatically generate Swift code for resources of your projects (like images, localised strings, etc), to make them type-safe to use.

Kingfisher

Kingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance to use a pure-Swift way to work with remote images in your next app.

IQKeyboardManager

While developing iOS apps, we often run into issues where the iPhone keyboard slides up and covers the UITextField/UITextView. IQKeyboardManager allows you to prevent this issue of keyboard sliding up and covering UITextField/UITextView without needing you to write any code or make any additional setup. To use IQKeyboardManager you simply need to add source files to your project.

Unit Tests

Unit tests have been implemented for several components like the view models, repos, and data sources.

CI/CD, Fastlane

The integration of Fastlane CI/CD enables automated builds, testing, and deployment of the app. This reduces the time and effort required for manual testing and deployment. Additionally, Fastlane implementation supports uploading the app to TestFlight and Firebase App Distribution, which provides a streamlined and efficient way to distribute the app to beta testers and stakeholders.

The implementation is located in /fastlane/Fastfile and other files in the folder.

Build Configurations

There are four different build configurations that are used to manage the product lifecycle: Debug, Testing, Staging, and Release. Each build configuration serves a specific purpose in the development process.

The Debug build configuration is used exclusively for development purposes, allowing developers to test and debug their code.

The Testing build configuration is used for testing the product by quality assurance (QA) testers. This build is more stable than the Debug build, but not yet ready for release.

The Staging build configuration is used for preparing the product for release. This build is more polished and stable than the Testing build, and is used for final testing and reviews.

The Release build configuration is used for production purposes, and is the final version of the product that is released to the public.

Each configuration has its unique bundle name to enable installing all of them on the same device.

And each build configuration has its own app name with a suffix that indicates the build configuration, such as "NAME-debug" or "NAME-testing". This naming convention helps to differentiate between the different build configurations.

Similarly, each build configuration has its own unique icon that indicates the build configuration.

By using different build configurations, developers can streamline the development process and ensure that the product is thoroughly tested and reviewed before being released to the public.

Crash Reporting

The AppCrashytics class serves as an essential interface for Crashlytics, a tool that helps track and analyze app behavior. It allows for the recording of any caught exceptions that occur within the app, providing valuable insights into potential issues and areas for improvement. By leveraging the AppCrashytics interface, developers can better understand how their app is behaving in real-world scenarios and take proactive measures to address any issues that may arise. This ultimately leads to a more stable and reliable app for users.

License

Apache License, Version 2.0

click to reveal License
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

   https://www.apache.org/licenses/LICENSE-2.0

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.

About

StockWise is a powerful app provides everything you need to track and monitor stocks, all in one convenient location. With user-friendly features and real-time data, StockWise is the perfect solution for anyone looking to stay ahead of the game. With StockWise, you'll have access to up-to-date stock prices, news, and trends, all in real-time.


Languages

Language:Swift 65.4%Language:Dart 10.3%Language:C++ 9.0%Language:CMake 7.4%Language:Ruby 6.5%Language:HTML 0.7%Language:C 0.6%Language:Shell 0.1%Language:Kotlin 0.0%Language:Objective-C 0.0%