-
Notifications
You must be signed in to change notification settings - Fork 3
Description
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 ourCell
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.
- Layout
- 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.
- Is this API more of a sugar-candy DSL that just makes it simpler to define your sections, or
- 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