Skip to content

Function Builder API #7

@shaps80

Description

@shaps80

Adding FunctionBuilder support

Introduction

I'd like to add support for the new function builder APIs.

Motivation

The current implementation requires the creation of a class with the inclusion of at least 1 method. In addition, it is required to create at least 1 cell class (and associated XIB?).

This doesn't sound like a lot but in practice it can still quickly become cumbersome when building reusable code.

Most often its necessary to define your own generic types on top of Composed's options:

final class PeopleSection: ArraySection<Person>, CollectionSectionProvider {

    func section(with traitCollection: UITraitCollection) -> CollectionSection {
        let cell = CollectionCellElement(section: self, dequeueMethod: .nib(PersonCell.self)) { cell, index, section in
            let element = section.element(at: index)
            cell.prepare(with: element)
        }
        return CollectionSection(section: self, cell: cell)
    }

}

Even in this rudimentary example, that's still a fair amount of code. What's important to capture here is:

  • Our data will be backed by an Array
  • Our data will be presented in a UICollectionView
  • Our data will be represented by a PersonCell

Proposed solution

Add a FunctionBuilder style DSL to simplify usage for an API consumer as well reduce the code required to achieve the same result.

The following represents an 'idea' rather than a concrete example. There are still a few areas that need to be worked out before this is possible, as such the following code is for demonstration purposes only and is subject to change.


Section(people) { index in
    Cell(.fromNib(PersonCell.self) { cell in
        cell.prepare(people[index])
    }
}

There are a few added benefits to this approach:

  • We 'inject' the data (in this case an Array)
  • Our closures are simpler, our section returns an index (to the element) and our cell returns an instance of the cell to configure
  • We no longer need to subclass in order to specify our backing data

Presentation

One thing to note, Cell needs to be inferred so the above wouldn't compile until it's inserted into a view. A nice solution to this might look like the following:

struct Dashboard: CollectionView {
    var content: Content  {
        Section(friends) { ... }
        Section(family) { ... }
    }

}

Using this approach we can see:

  • The use of a struct removes reference semantics and the potential for retain cycles
  • We can infer that a UICollectionView is intended for the presentation
  • We can also now infer that a UICollectionViewCell is what we're expecting for our Cell configuration
  • We can easily compose multiple sections

Headers & Footers

However another added benefit to using the view to infer our Section type is that we can also now determine what 'features' our Section might have.

For example, we know that our UICollectionView supports header/footer elements:

struct Dashboard: CollectionView {
    var header: Header {
        Header(.fromNib(PeopleHeader.self)) { header in
            header.titleLabel.text = "People"
        }
    }

    // and to add the header to the section...
   
    var content: Content  {
        Section(header: header) { ... }
    }
}

We could even go further, providing a convenience header type:

Section(header: Text("People"))

Static Views

One final advantage over this API is that it makes it trivial to build both dynamic as well as static Section's.

Section {
    Cell(.fromNib(PersonNameCell.self) { cell in
        cell.prepare(person.name)
    }

    Cell(.fromNib(PersonAgeCell.self) { cell in
        cell.prepare(person.age)
    }
}

It may even be possible to implement ForEach such that mixing static and dynamic content would become possible.


Existing Sections

In the current implementation we have a few Section types that define the backing data that will be used for that section.

  • SingleElementSection
  • ArraySection
  • ManagedSection

As well, we have two SectionProvider types that help us compose sections:

  • ComposedSectionProvider
  • SegmentedSectionProvider

Using the proposed implementation, we can see all of these become completely unnecessary.

SingleElementSection

A SingleElementSection can be replaced by simply defining a static section:

Section {
    Cell(.fromNib(PersonNameCell.self) { cell in
        cell.prepare(person.name)
    }
}

ArraySection & ManagedSection

Both can be defined by simply providing the relevant data to the Section

ComposedSectionProvider

Simply including multiple sections removes the need for a specific type as well.

SegmentedSectionProvider

That leaves us with just one final type. The purpose of this type is to hold onto 1 or more child Section's while keeping only 1 active at a time.

Well, this is easily achieved with the above implementation simply with the use of conditional statements.

Section(...)

if isVisible {
    Section(...)
}

Section(...)

Conclusion

It's now apparent that this implementation should remove the need for an API consumer (at least) to ever have any knowledge of multiple Section types. Instead focus on providing the data (or not) for a Section and specifying which Cell they want to be used for representing the element at a specified index.

As for composition, using Swift knowledge you already have, you should easily be able to build composable Section's using nothing more than conditional statements.

The use of FunctionBuilder APIs also removes the additional syntax of brackets and comma's that would otherwise (and currently) litter your code.

Behaviours

ComposedUI also extends your Section through the use of protocol conformance to provide behaviours like editing, selection, drag and drop and more.

One potential solution here would be the use of a Modifier-style API as seen in SwiftUI:

struct PeopleSection: CollectionSection {
    var header: Header {
        Text("People")
            .attributes(...)
            .padding(.horizontal, 20)
    }
    
    var content: Content {
        Section(header: header, people) { index in
            Cell(.fromNib(PersonCell.self)) { ... }
                .onSelection { ... }
                .onDrag { ... }
                .onAppear { ... }
                .contextMenu { ... }
        }
    }
}

There are a few obvious benefits to this approach:

  • Call order is not important
  • Behaviour is still additive (not implementing the modifier, implicitly disables it)
  • Its a familiar API (if you've used SwiftUI) and its extremely easy to discover
  • In future versions Composed will likely support SwiftUI and this API will 'feel' a lot more familiar and consistent.

Final Thoughts

This approach is not only simpler and cleaner, its also much more user friendly and discoverable. It removes a few key pain points for users as well:

  • No need to subclass, or even use a class
  • No need to conform to other protocols (that you always need to 'know' about)
  • Data, indexing and presentation are truly separated, only coming together at the point where you embed them in a view
  • You 'describe' the sections more naturally

One last thing...

There are 2 other areas that still need to be thought through.

  1. Layout
  2. Environment

Layout, can likely be solved by initialising the view with a layout (if required). The bigger question is how to tie individual section layouts to the outer view layout. However we've solved it already, so I'm sure this can be worked through.

Environment variables are passed to functions or closures currently. These could replaced by something more like @Environment available in SwiftUI where Composed ensures the variables are set automatically for you at the right moment. I'm not sure atm if this is possible or something the compiler does for free in SwiftUI alone. I will need to investigate this further. Alternatively, the functions/closures could continue to provide these values as required.

Considerations

The current approach to handle section updates and ensure they get propagated up to the coordinator is to use the delegate pattern. In order to support this 1 of 2 things should be considered.

  1. Is this API more of a sugar-candy DSL that just makes it simpler to define your sections, or
  2. Is this a complete re-architecture of how Composed is implemented

My current instincts are to consider the usage of Combine and the new Swift diffing APIs, however this would mean dropping support for iOS <13. I'm not adverse to this, but it should be considered carefully.

Source compatibility

This is likely to cause breaking API changes and require significant renaming in both ComposedUI and Composed. As such its planned for 2.0.

Alternatives considered

N/A

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions