Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component communication #72

Merged
merged 25 commits into from
Jul 19, 2023
Merged

Conversation

Supereg
Copy link
Member

@Supereg Supereg commented Jul 15, 2023

Component communication

♻️ Current situation & Problem

Currently, there is no easy way for a Component to access features or information provided by another Component.

I illustrate the usefulness using a real-world example we are currently facing with SpeziAccount. A AccountConfiguration is used by the user to configure the SpeziAccount subsystem which in turn does everything necessary to set up the request AccountServices to be accessible by the App's views. While one may just provide off-the-shelve AccountServices using the initializer of the configuration object, there might be instances where an third-party AccountService is controlled and configured by a Component. In such cases it should be possible for the user to place the Component in the Configuration closure as usual with the AccountConfiguration querying AccountService instances from all configured Components.

let config = Configuration(standard: TestAppStandard()>) {
    AccountConfiguration {
        SimpleAccountService()
    }

    FirebaseComponent() // a component preforming necessary initialization steps for its AccountService
}

💡 Proposed solution

In this PR we introduce the @Provide and @Collect property wrappers that can be used to pass around information or functionality between Components. The @Provide property wrapper is used to supply a single Value (or Value? if it e.g. only supplies it conditionally) or multiple values ([Value]) to be collected by other Components.
Another Component can then use @Collect with a type of [Value] to collect all the provided items.

Here is a short code example:

class FirebaseComponent<ComponentStandard: Standard>: Component {
    @Provide private var accountService: any AccountService

    init() {
        // @Provide have to be initialized before the components configure() method is called
        self.accountService = FirebaseAccountService() // may also be initialize inline
    }

    func configure() {
        // do firebase initializations 
        accountService.configure() // as its a reference type, we can adjust state
    }
}

class AccountConfiguration<ComponentStandard: Standard>: Component {
    @Collect private var accountServices: [any AccountService]

    func configure() {
        // @Collect can be used within the configure method
        // ...
    }
}

While the example shows the usage of a reference type, this feature can also easily be used with value types (though then, no modifications post the collection phase are possible).

⚙️ Release Notes

  • Added @Provide and @Collect property wrappers for easy to use inter-Component communication
  • Added a generalized and reusable SharedRepository pattern to easily implement typed collections.

➕ Additional Information

Shared Repository Pattern

This PR additionally introduces the concept of the SharedRepository software pattern based on Buschmann et al. (Pattern-Oriented Software Architecture: A Pattern Language for Distributed Computing). We provide a variety of different types of KnowledgeSources which are stored in SharedRepository implementations. Each shared repository defines a SharedRepositoryAnchor it expects from an implementing KnowledgeSource.

This instantiation of the shared repository pattern replaces the previous TypedCollection implementation. Further, it will be of equal use for SpeziAccount (storing arbitrary account details). The current draft PR introduces a similar concept. This PR helps to generalize this software pattern and increase code reuse across the framework ecosystem.

Alternative Considered

As with the previous TypeCollection implementation, a SharedRepository allows for a collect(allOf:) query which is, e.g., internally used to query all Components that conform to LifecycleHandler. Instead of the proposed system, we could have created a property wrapper that just collects all components that conform to a given protocol. I decided against this approach considering the following points:

  • The alternative solution would not allow to prohibit propagation of information (only by the visibility of a protocol itself).
  • The alternative solution would allow to circumvent the current @Dependency system (or we would need to consider '@Collect` declarations in the execution order).
  • The alternative system would be limited to just retrieve protocol conformances.
  • The proposed solution decouples propagated information from the Component by default allowing for arbitrary information (value types, reference types, information, functionality).

Related PRs

Testing

Test cases were adapted but still need to be extended for newly added code.

Reviewer Nudging

There are two separate parts to the PR:

  • The SharedRepository data structure
  • The Collect and Provide property wrappers. You may look at the tests to easily glance what their use case is.

Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

@Supereg Supereg force-pushed the feature/inter-module-communication branch from cb89973 to 1f5a263 Compare July 15, 2023 23:39
@codecov
Copy link

codecov bot commented Jul 16, 2023

Codecov Report

Merging #72 (03e84f3) into main (162c932) will decrease coverage by 0.61%.
The diff coverage is 92.24%.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #72      +/-   ##
==========================================
- Coverage   89.45%   88.83%   -0.61%     
==========================================
  Files          30       44      +14     
  Lines         635      743     +108     
==========================================
+ Hits          568      660      +92     
- Misses         67       83      +16     
Files Changed Coverage Δ
Sources/Spezi/Adapter/Adapter.swift 0.00% <ø> (ø)
Sources/Spezi/Adapter/DataChange.swift 100.00% <ø> (ø)
Sources/Spezi/Adapter/SingleValueAdapter.swift 100.00% <ø> (ø)
Sources/Spezi/Configuration/Component.swift 100.00% <ø> (ø)
Sources/Spezi/Configuration/Configuration.swift 100.00% <ø> (ø)
Sources/Spezi/DataSource/DataSourceRegistry.swift 50.00% <ø> (ø)
...Provider/DataStorageProvidersPropertyWrapper.swift 100.00% <ø> (ø)
...dule/Capabilities/Lifecycle/LifecycleHandler.swift 100.00% <ø> (ø)
...s/ObservableObject/ObservableObjectComponent.swift 86.37% <ø> (ø)
Sources/Spezi/Spezi/SpeziSceneDelegate.swift 83.34% <ø> (ø)
... and 25 more

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 162c932...03e84f3. Read the comment docs.

@PSchmiedmayer PSchmiedmayer mentioned this pull request Jul 16, 2023
1 task
@PSchmiedmayer PSchmiedmayer self-requested a review July 19, 2023 03:27
Copy link
Member

@PSchmiedmayer PSchmiedmayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for all the changes there @Supereg, a huge step forward 🚀

I had a few comments here and there. Overall the PR looks great and feel free to merge it after addressing some of the comments and iterating on some of the features. Thank for all the work and time that goes into this project.

Very cool to see some of the @Apodini aspects making a reappearance here; more and more of the learnings from that project are applied to @StanfordSpezi, very nice!

CONTRIBUTORS.md Show resolved Hide resolved
Sources/Spezi/Adapter/DataChange.swift Show resolved Hide resolved
Sources/Spezi/Configuration/Component.swift Outdated Show resolved Hide resolved
@Supereg Supereg marked this pull request as ready for review July 19, 2023 15:02
@Supereg Supereg added the enhancement New feature or request label Jul 19, 2023
@Supereg Supereg requested a review from PSchmiedmayer July 19, 2023 18:09
@Supereg
Copy link
Member Author

Supereg commented Jul 19, 2023

@PSchmiedmayer I addresses all the feedback. Wanted to re-request your review just to make sure 🚀

@PSchmiedmayer
Copy link
Member

Thank you! I will have some time in the late afternoon to re-review the PR. If it is fine with you I can fix smaller issues that I might find myself and then merge the PR after that? Maybe I don't even have some feedback and could also directly merge it 👍

@Supereg Supereg changed the title Inter-Component communication Component communication Jul 19, 2023
Copy link
Member

@PSchmiedmayer PSchmiedmayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing job with the PR @Supereg, very nice additions and a superb code quality!

@PSchmiedmayer PSchmiedmayer enabled auto-merge (squash) July 19, 2023 22:46
@PSchmiedmayer PSchmiedmayer merged commit d02b67a into main Jul 19, 2023
@PSchmiedmayer PSchmiedmayer deleted the feature/inter-module-communication branch July 19, 2023 23:04
Supereg added a commit to StanfordSpezi/SpeziAccount that referenced this pull request Sep 14, 2023
# Unified Account View with rethought Account Service Model

## ♻️ Current situation & Problem

Currently, `SepziAccount` exposes two distinct views: `Login` and
`SignUp`. While visually identical (expect for customizations), the
AccountService buttons navigate to the respective Login or SignUp views
for data entry. This introduces several navigation steps for the user
even for simple workflows (e.g. just signing in a returning user).
Further, it is not clear where to place Identity Provider buttons (e.g.,
Sign in with Apple), as these create a new account or just sign you in
to your existing one with the same, single button.

## 💡 Proposed solution
This PR makes the following changes:

* Unifying `Login` and `SignUp` views to be represented by a single
view, placing identity providers on the same page as regular Account
Services, the `AccountSetup` view.
* Restructure the Account Service Model:
* Make the necessary preparations to support third-party Identity
providers
* Introduce the concept of an `EmbeddableAccountService` and a new
`KeyPasswordBasedAccountService` as new abstraction layers.
This replaces the current `UsernamePasswordAccountService` and
`EmailPasswordAccountService` implementations. Those we really only Mock
implementations which provided extensibility through class inheritance.
This was replaced by providing the same functionality with a hierarchy
of protocols being more powerful and providing much more flexibility.
Further, we introduce new Mock Account services (e.g., used by
PreviewProviders) which more clearly communicate their use through their
updated name.
* Move the UI parts of an AccountService out into a distinct abstraction
layer (`AccountSetupViewStyle`s).
* Introduce the `Account Value` (through `AccountKey` implementations)
abstraction to support arbitrary account values
* They provide a name, a category (for optimized placement in the UI),
and a initial value
* The provide the UI components do display (`DataDisplayView`) and setup
or edit (`DataEntryView`) account values
* The provide easy to use accessors using extensions on `AccountKeys`
and `AccountValues`
* Optimized API surface for end-users:
  * Exposes the `signedIn` published property (same as previously)
* Exposees the `details` shared repository to access the
`AccountDetails` of the currently logged in use. This also provides
access to the `AccountService` and its configuration that was used to
setup the user account.
* Provide a new `AccountOverview` view:
  * Allows to view arbitrary account values
  * Allows to edit arbitrary account values
  * Provide Logout and account Removal functionality
* Enabled through StanfordSpezi/Spezi#72, one
can easily configure `SpeziAccount` centrally while other configured
components can provide their AccountServices via [Component
Communication](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component#Communication).

This PR tries to provide extensive documentation for all areas.

To provide some insights into the new user-facing API surface.

This is how you would configure Spezi Account now:
```swift
class YourAppDelegate: SpeziAppDelegate {
    override var configuration: Configuration {
        AccountConfiguration(configuration: [
            // the user defines what account values are used and if they are `required`, `collected` or just `supported`
            .requires(\.userId),
            .requires(\.password),
            .requires(\.name),
            .collects(\.dateOfBirth),
            .collects(\.genderIdentity)
        ])
    }
}
```

Below is an example on how to use the account setup view:
```swift
struct MyOnboardingView: View {
    var body: some View {
        AccountSetup {
           NavigationLink {
               // ... next view if already signed in
           } label: {
               Text("Continue")
           }
        }
    }
}
```

Lastly, the account overview is equally as easy to use:
```swift
struct MyView: View {
    var body: some View {
        AccountOverview()
    }
}
```

## ⚙️ Release Notes 
* New, unified `AccountSetup` view
* New `AccountOverview` view
* Introduce new `AccountService` abstraction
* Support arbitrary account values through `AccountKey`-based shared
repository implementation

## ➕ Additional Information

### Related PRs and Issues
* This PR provides the groundwork necessary for
StanfordSpezi/SpeziFirebase#7
* The updated Firebase implementation:
StanfordSpezi/SpeziFirebase#8

### Testing
Substantial UI tests were updated and added.

### Reviewer Nudging
As this is quite a large PR it would make sense to start reviewing by
reading through the documentation. First of all reviewing the
documentation itself (structure and readability). Code example on the
DocC article already explain a lot of the concepts.
The areas that sparked your attention, where you think that something is
off or hard to understand, I would recommend to deep dive into the
respective code area.

### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Co-authored-by: Paul Schmiedmayer <PSchmiedmayer@users.noreply.github.com>
NikolaiMadlener pushed a commit to NikolaiMadlener/SpeziAccount that referenced this pull request Oct 13, 2023
…ezi#7)

# Unified Account View with rethought Account Service Model

## ♻️ Current situation & Problem

Currently, `SepziAccount` exposes two distinct views: `Login` and
`SignUp`. While visually identical (expect for customizations), the
AccountService buttons navigate to the respective Login or SignUp views
for data entry. This introduces several navigation steps for the user
even for simple workflows (e.g. just signing in a returning user).
Further, it is not clear where to place Identity Provider buttons (e.g.,
Sign in with Apple), as these create a new account or just sign you in
to your existing one with the same, single button.

## 💡 Proposed solution
This PR makes the following changes:

* Unifying `Login` and `SignUp` views to be represented by a single
view, placing identity providers on the same page as regular Account
Services, the `AccountSetup` view.
* Restructure the Account Service Model:
* Make the necessary preparations to support third-party Identity
providers
* Introduce the concept of an `EmbeddableAccountService` and a new
`KeyPasswordBasedAccountService` as new abstraction layers.
This replaces the current `UsernamePasswordAccountService` and
`EmailPasswordAccountService` implementations. Those we really only Mock
implementations which provided extensibility through class inheritance.
This was replaced by providing the same functionality with a hierarchy
of protocols being more powerful and providing much more flexibility.
Further, we introduce new Mock Account services (e.g., used by
PreviewProviders) which more clearly communicate their use through their
updated name.
* Move the UI parts of an AccountService out into a distinct abstraction
layer (`AccountSetupViewStyle`s).
* Introduce the `Account Value` (through `AccountKey` implementations)
abstraction to support arbitrary account values
* They provide a name, a category (for optimized placement in the UI),
and a initial value
* The provide the UI components do display (`DataDisplayView`) and setup
or edit (`DataEntryView`) account values
* The provide easy to use accessors using extensions on `AccountKeys`
and `AccountValues`
* Optimized API surface for end-users:
  * Exposes the `signedIn` published property (same as previously)
* Exposees the `details` shared repository to access the
`AccountDetails` of the currently logged in use. This also provides
access to the `AccountService` and its configuration that was used to
setup the user account.
* Provide a new `AccountOverview` view:
  * Allows to view arbitrary account values
  * Allows to edit arbitrary account values
  * Provide Logout and account Removal functionality
* Enabled through StanfordSpezi/Spezi#72, one
can easily configure `SpeziAccount` centrally while other configured
components can provide their AccountServices via [Component
Communication](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component#Communication).

This PR tries to provide extensive documentation for all areas.

To provide some insights into the new user-facing API surface.

This is how you would configure Spezi Account now:
```swift
class YourAppDelegate: SpeziAppDelegate {
    override var configuration: Configuration {
        AccountConfiguration(configuration: [
            // the user defines what account values are used and if they are `required`, `collected` or just `supported`
            .requires(\.userId),
            .requires(\.password),
            .requires(\.name),
            .collects(\.dateOfBirth),
            .collects(\.genderIdentity)
        ])
    }
}
```

Below is an example on how to use the account setup view:
```swift
struct MyOnboardingView: View {
    var body: some View {
        AccountSetup {
           NavigationLink {
               // ... next view if already signed in
           } label: {
               Text("Continue")
           }
        }
    }
}
```

Lastly, the account overview is equally as easy to use:
```swift
struct MyView: View {
    var body: some View {
        AccountOverview()
    }
}
```

## ⚙️ Release Notes 
* New, unified `AccountSetup` view
* New `AccountOverview` view
* Introduce new `AccountService` abstraction
* Support arbitrary account values through `AccountKey`-based shared
repository implementation

## ➕ Additional Information

### Related PRs and Issues
* This PR provides the groundwork necessary for
StanfordSpezi/SpeziFirebase#7
* The updated Firebase implementation:
StanfordSpezi/SpeziFirebase#8

### Testing
Substantial UI tests were updated and added.

### Reviewer Nudging
As this is quite a large PR it would make sense to start reviewing by
reading through the documentation. First of all reviewing the
documentation itself (structure and readability). Code example on the
DocC article already explain a lot of the concepts.
The areas that sparked your attention, where you think that something is
off or hard to understand, I would recommend to deep dive into the
respective code area.

### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Co-authored-by: Paul Schmiedmayer <PSchmiedmayer@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants