Giter Site home page Giter Site logo

composed-swift / composed Goto Github PK

View Code? Open in Web Editor NEW
7.0 2.0 3.0 581 KB

A Swift framework for composing data.

License: Other

Swift 97.28% Ruby 2.72%
swift swift-package-manager spm composition protocol-oriented-programming composition-api composed

composed's Introduction

Composed is a protocol oriented framework for composing data from various sources in our app. It provides various concrete implementations for common use cases.

If you prefer to look at code, there's a demo project here: ComposedDemo

The library bases everything on just 2 primitives, Section and SectionProvider.

The primary benefits of using Composed include:

  • The library makes heavy use of protocol-oriented design allowing your types to opt-in to behaviour rather than inherit it by default.
  • From an API perspective you generally only care about a single section at a time, and so it's irrelevant to you how it's being composed.
  1. Getting Started
  2. Behind the Scenes
  3. User Interfaces

Getting Started

Composed includes 3 pre-defined sections as well as 2 providers that should satisfy a large majority of applications.

Sections

ArraySection Represents a section that manages its elements via an Array. This type of section is useful for representing in-memory data.

ManagedSection Represents a section that provides its elements via an NSFetchedResultsController. This section is useful for representing data managed by CoreData.

SingleElementSection Represents a section that manages a single element. This section is useful when only have a single element to manage. Hint: Use Optional<T> to represent an element that may or may not exist.

Providers

ComposedSectionProvider Represents an collection of Section's and SectionProvider's. The provider supports infinite nesting, including other ComposedSectionProvider's. All children will be active at all times, so numberOfSections and numberOfElements(in:) will return values representative of all children.

SegmentedSectionProvider Represents an collection of Section's and SectionProvider's. The provider supports infinite nesting, including other SegmentedSectionProvider's. One or zero children may be active at any time, so numberOfSections and numberOfElements(in:) will return values representative of the currenly active child only.

Example

Lets say we wanted to represent a users contacts library. Our contacts will have 2 groups, family and friends. Using Composed, we can easily model that as such:

let family = ArraySection<Person>()
family.append(Person(name: "Dad"))
family.append(Person(name: "Mum"))

let friends = ArraySection<Person>()
friends.append(Person(name: "Best mate"))

At this point we have 2 separate sections for representing our 2 groups of contacts. Now we can use a provider to compose these 2 together:

let contacts = ComposedSectionProvider()
contacts.append(family)
contacts.append(friends)

That's it! Now we can query our data using the provider without either of the individual sections even being aware that they're now contained in a larger structure:

contacts.numberOfSections        // 2
contacts.numberOfElements(in: 1) // 1

If we want to query individual data in a section (assuming we don't already have a reference to it):

let people = contacts.sections[0] as? ArraySection<Person>
people.element(at: 1)            // Mum

Note: we have to cast the section to a known type because SectionProvider's can contain any type of section as well as other nested providers.

Opt-In Behaviours

If we now subclass ArraySection, we can extend our section through protocol conformance to do something more interesting:

final class People: ArraySection<Person> { ... }

protocol SelectionHandling: Section { 
    func didSelect(at index: Int)
}

extension People: SelectionHandling {
	func didSelect(at index: Int) {
		let person = element(at: index)
		print(person.name)
	}
}

In order to make this work, something needs to call didSelect, so for the purposes of this example we'll leave out some details but to give you a preview for how you can build something like this yourself:

// Assume we want to select the 2nd element in the 1st section
let section = provider.sections[0] as? SelectionHandling
section?.didSelect(at: 1)        // Mum

Composed is handling all of the mapping and structure, allowing us to focus entirely on behavious and extension.

Behind the Scenes

Section

A section represents exactly what it says, a single section. The best thing about that is that we have no need for IndexPath's within a section. Just indexes!

SectionProvider

A section provider is a container type that contains either sections or other providers. Allowing infinite nesting and therefore infinite possibilities.

Mappings

Mappings provide the glue between your 'tree' structure and the resulting flattened structure. Lets take a look at an example.

// we can define our structure as such:
- Provider
    - Section 1
    - Provider
        - Section 2
        - Section 3
    - Section 4

// mappings will then convert this to:
- Section 1
- Section 2
- Section 3
- Section 4

Furthermore, mappings take care of the conversion from local to global indexes and more importantly IndexPath's which allows for even more interesting use cases.

To find out more, checkout ComposedUI which provides user interface implementations that work with UICollectionView and UITableView, allowing you to power an entire screen from simple reusable Sections.

composed's People

Contributors

josephduffy avatar shaps80 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

composed's Issues

Adding `Suspendible` support

Adding Suspendible support

Introduction

Composed should introduce the concept of a Section being Suspendible in order to prevent user-driven events triggering duplicate events in UIKit.

Motivation

ManagedSection is backed by CoreData and currently provides dedicated functions for suspending updates in order to support user-driven events (e.g. reordering).

However the general pattern used by Composed is to 'discover' features through the use of a protocol. To bring this inline with the rest of Composed, I'm proposing we introduce a protocol to add support for this:

public protocol Suspendible {
    func suspend()
    func resume()
}

In addition I feel we should add similar methods to the appropriate Coordinator's:

This is just to provide a simple convenience for the API consumer. The implementation would be:

extension CollectionCoordinator {
    func suspend() {
        sectionProvider.sections
            .compactMap { $0 as? Suspendible }
            .forEach { $0.suspend() }
    }

    func resume() {
        sectionProvider.sections
            .compactMap { $0 as? Suspendible }
            .forEach { $0.resume() }
    }
}

Currently only ManagedSection would implement this protocol however formalising this approach allows custom Section's to piggy-back on this behaviour as well.

Source compatibility

This should be purely additive.

Alternatives considered

N/A

Improve extensions for RandomAccessCollection

Improve extensions for RandomAccessCollection

Introduction

We already provide some enhancements on ArraySection to make it behave more like an array. In addition we could add further conformance on Section itself using where clauses.

E.g.

extension Section where Self: RandomAccessCollection {
    public var numberOfElements: Int { return count }
}

Motivation

ComposedMedia sections don't currently conform to the recent updates due to missing auto-conformance. Adding RandomAccessCollection conformance to Section fixes this and other potential sections in the future.

Source compatibility

This is an additive feature.

`CollectionCoordinatorDelegate`'s background view does not behave as documented

Describe the bug

The documentation of CollectionCoordinatorDelegate.coordinator(_:backgroundViewInCollectionView:) states:

Return a background view to be shown in the UICollectionView when its content is empty. Defaults to nil

This is wrong because the background view is set even when the collection view is not empty.

It also causes what I could class as a bug: whenever CollectionCoordinator.prepareSections() is called it will reset the background view, even when the delegate is nil.

To Reproduce

Use CollectionCoordinator

Expected behavior

  1. Do not reset backgroundView when delegate is nil or the default implementation of CollectionCoordinatorDelegate.coordinator(_:backgroundViewInCollectionView:) is used
  2. Only show the background when the collection view is empty

Environment

  • OS Version: Any
  • Library Version: 1.x, 2.x
  • Device: Any

How to use `ComposedSectionProvider` with `Section`s that update?

When calling ComposedSectionProvider.append(_:) with a Section the updateDelegate is not set (and ComposedSectionProvider does not conform to SectionUpdateDelegate). This causes any updates propagated by the section to not be honoured.

It looks like these updates should be propagated up, performing mapping as required. Does SectionProviderUpdateDelegate need to inherit SectionUpdateDelegate to ensure these updates are not lost?

Project is unlicenced

There's no licence associated with the project (or the others in this organisation). Could one be added?

Crash when update occurs as side effect of view configuration

A crash can happen when an update occurs as a side effect of a view being configured, e.g. the following will trigger a crash:

  • Batch update with insert 0, 0
  • View for item 0, 0 deletes item 0, 0 during configuration

A somewhat more reasonable example would be something in a header (e.g. a series of tabs) triggering the update as a setup of initial state.

This is because the updates closure of performBatchUpdates is still being called so the delete is processed in the context prior to the insert. This is generally a misuse of UICollectionView and is hard to create a correct fix.

Consumers should be discouraged from performing updates during configuration, but it would be nice if this could be detected and the crash prevented.

Original message I'm trying to debug batch updates and seeing some strange behaviour, maybe you can provide some insight @shaps80?

The updates are applied in https://github.com/opennetltd/Composed/blob/batch-collection-view-updates-closure/Sources/ComposedUI/CollectionView/CollectionCoordinator.swift#L254, which can be simplified as:

print("Calling `performBatchUpdates`")
collectionView.performBatchUpdates({
    print("Starting batch updates")
    // ... apply updates
    print("Batch updates have been applied")
}, completion: { [weak self] isFinished in
    print("Batch updates completed. isFinished: \(isFinished)")
})
print("`performBatchUpdates` has been called")

I have then seen logs like:

Calling `performBatchUpdates`
Starting batch updates
Batch updates have been applied

Calling `performBatchUpdates`
Starting batch updates
Batch updates have been applied

`performBatchUpdates` has been called
`performBatchUpdates` has been called

Calling `performBatchUpdates`
Starting batch updates
Batch updates have been applied

**Crash**

Note how Starting batch updates is printed twice before the `performBatchUpdates` has been called. How can this be possible?

This made me look at this a little more, and only raised more questions. The updates closure has to be sync because it's not escaping (although I don't see this in the actual header and it is optional, but somehow Swift thinks it's not escaping) and Batch updates have been applied is always printed before another update.

It looks like collection view is blocking the return from performBatchUpdates and multiple calls are possible, but this is all being done on the main thread. This makes me think my method of testing is wrong but I can't see what it could be.

Note that the crash occurs when updates are applied as:

  • Insert section 0
  • Insert element 0,0
  • Reload element 0,0

Since the completion hasn't been called yet I think it hasn't committed the insert yet and it crashes with a message stating that it can't delete element 0,0 because it doesn't exist yet.

One solution is to wait for completion to be called before applying more updates but this would then make all updates within Composed potentially async and introduce delay (due to waiting for layout) that isn't required the majority of the time.

[BUG]

Describe the bug

[A clear and concise description of what the bug is]

To Reproduce

[Steps to reproduce the issue]

Expected behavior

[A clear and concise description of what you expected to happen]

Environment

  • OS Version: [e.g. iOS 12.4]
  • Library Version: [e.g. 1.0.3
  • Device: [e.g. iPhone XS]

Additional context

[Add any other context about the problem here]

Merge Composed, ComposedUI, and ComposedLayouts packages

Merge in to a single package

Introduction

Move code from ComposedUI and ComposedLayouts in to this repo (Composed)

Motivation

2 main reasons:

  • Development
    • If a change is made across multiple projects multiple releases are required, minimum versions updated etc.
    • Keeping changes like CI in-sync (such as updating the Xcode version) means updating multiple repos
  • Consumers
    • Discovering the available modules is not immediately obvious
      • This is especially true if more modules are added in the future
    • Keeping multiple dependencies up-to-date adds overhead

The change would be fairly easy (copy/paste, small update to Package.swift).

Source compatibility

I think this is a breaking change; import ComposedUI would be ambiguous (?)

Alternatives considered

None

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.