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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

The great flattening. [[Model]] -> [Model] #48

Merged
merged 14 commits into from
Jun 7, 2017

Conversation

danielrhammond
Copy link
Contributor

@danielrhammond danielrhammond commented May 14, 2017

Overview

Took a crack at #theflattening on my 馃洬 home. This is an initial cut of transitioning to where the most common implementations of ModelCollection don't have to operate on sectioned data but can instead store and operate on a simple array ([Model])

This is a first draft, wanted to push up a bit early to get some feedback on direction. Some known remaining TODOs before merge:

  • Get alignment on approach vs alternatives
  • Documentation / code comment update pass
  • Dry run of migration on our apps to see if there's any unforeseen challenges
  • Audit tests for any redundant (or missing) cases

Approach to transition

Decided to keep the diff engine sectioned, this meant ModelCollection (or the datasource?) needed to still provide some concept of sections so the previous property on ModelCollection sections: [[Model]] remains where the default implementation is just a wrapper on the new models: [Model] property.

Alternatives Considered

Option A: Protocol + Default Conformance

Add a new protocol SectionedDataSource which could look something like:

public protocol SectionedModelCollection {
    var state: [ModelCollectionState] { get }
}

and then default conformance for a ModelCollection could be as simple as:

extension ModelCollection: SectionedModelCollection {
    public var state: [ModelCollectionState] { return [state] }
}

This would be ideal since you could have the common single-sectioned model collections be completely obvlivious to the nature of sections while still overriding that in order to support them in cases where its valuable (ex: a MultiplexedModelCollection). Unfortunately swift doesn't allow adding conformance to a protocol via an extension. Which removes a lot of the awesomeness of this solution.

Option B: Sectioned as root protocol/interface

Make ModelCollection inherit directly from SectionedModelCollection:

public protocol ModelCollection: SectionedModelCollection {

    // MARK: Observable

    func addObserver(_ observer: @escaping  CollectionEventObserver) -> CollectionEventObserverToken
    func removeObserver(with token: CollectionEventObserverToken)

    var collectionId: ModelCollectionId { get }

    // MARK: State

    /// Current state of the model collection. Typically used by reference-type implementations, as value-type
    /// implementations stay `.Loaded`.
    var state: ModelCollectionState { get }
}

public protocol SectionedModelCollection: class {
    func addSectionedObserver(_ observer: @escaping  ???) -> Token
    func removeSectionedObserver(with token: Token)
    var sectionedState: [ModelCollectionState] { get }
}

public extension ModelCollection {
    func addObserver(_ observer: @escaping  ???) -> Token { ... }
    func removeObserver(with token: Token) { ... }
    public var sectionedState: [ModelCollectionState] { return [state] }
}

This has some nice properties, in that it allows all existing model collections to remain mostly unchanged, while reaping the benefits of being single-sectioned for the common code paths. However it has a few downsides:

  1. Requires to have two observation types/paths. Due to model collection state changing to support the single section world, a model collection would need to have a way to observe it for updates while also providing a way to observe the sectioned data for updates. While it is possible to provide a default implementation for this, it still will be pretty messy/noisy on the client side of this API.
  2. Requires more changes to CollectionViewModelDataSource/ViewController
  3. There would be some additional friction where a concrete implementation of SectionedModelCollection couldn't be used/passed everywhere a ModelCollection would.

Option C: Remove concept of sections from Pilot

If we remove sections from the diff engine, it becomes pretty easy to flatten all references to [[Model]] in the app and delete a bunch of code. However, this comes at a pretty significant tradeoff for future use-cases where using sections makes a lot of sense, and didn't seem worth biting off at this moment.

Copy link
Contributor

@wkiefer wkiefer left a comment

Choose a reason for hiding this comment

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

Awesome work - thanks for digging into this!

I'm definitely onboard with Option A. Something like what you mentioned:

public protocol SectionedModelCollection {
    var sectionState: [ModelCollectionState] { get }
}

To get the "free" section support on ModelCollections, we could flip conformance like this:

extension SectionedModelCollection where Self : ModelCollection {
    public var sectionState: [ModelCollectionState] {
        return [state.models]
    }
}

which would require classes opting in to section support (which is probably fine). And then something like this could make code that requires sections (e.g. diff engine) easier, or "free":

extension ModelCollection {
    public func withSections() -> SectionedModelCollection {
        if let sectionedModel = self as? SectionedModelCollection {
            return sectionedModel
        }
        return StaticSectionedModelCollection(self)
    }
}

Long way of saying, this is great. Let's run with it!

/// Returns a dictionary mapping `ModelId`s to their index path within the given `sections` object.
public func generateModelIdToIndexPathMapForSections(_ sections: [[Model]]) -> [ModelId: ModelPath] {
/// Returns a dictionary mapping `ModelId`s to their index path within the given `models` object.
public func generateModelIdToIndexPathMapForSections(_ models: [Model]) -> [ModelId: ModelPath] {
Copy link
Contributor

Choose a reason for hiding this comment

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

We could remove this in the name of a smaller API surface (and it's not hard for apps to write). Also, we don't use it anymore.


/// Returns a dictionary mapping item `ModelId`s to their index path within the target `ModelCollection`.
public var modelIdToIndexPathMap: [ModelId: ModelPath] {
return generateModelIdToIndexPathMapForSections(sections)
return generateModelIdToIndexPathMapForSections(models)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above. Could remove modelIdToIndexPathMap

}

/// Convenience methods to return the total number of items in a `ModelCollection`.
/// TODO:(danielh) deprecate?
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a fan of removing this now, while we're making breaking changes.

}
}
return true
return models.isEmpty
Copy link
Contributor

Choose a reason for hiding this comment

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

we also have ModelCollectionState.isEmpty.. wonder if that's too redundant.

return typed
}
}
guard indexPath.modelSection == 0 else { return nil } // TODO:(danielh) throw/log error?
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about
a) moving these index path related methods to SectionedModelCollection ?
b) moving the three indexpath methods to a protocol and sectioned model collection & model collection can have their own implementations?

// swiftlint:enable nesting

let initialReducedStates = CollectionStateReduction()
let reducedStates = substates.reduce(initialReducedStates) { prevCollectionStateCounts, state in
Copy link
Contributor

Choose a reason for hiding this comment

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

until swift 4 reduce improvements land, should this be a for substate in substates?


extension ComposedModelCollection {

public static func multiplexing(_ modelCollections: [ModelCollection]) -> ComposedModelCollection {
Copy link
Contributor

Choose a reason for hiding this comment

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

馃憤

// The diff algorithm does not move sections around at this time. If sections had IDs,
// moving sections would be possible.
// The diff algorithm does not move models around at this time. If models had IDs,
// moving models would be possible.
Copy link
Contributor

Choose a reason for hiding this comment

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

this comment should still refer to sections

// First, make sure there are the right number of sections. This code always adds or removes sections
// from the end. In the future, if sections themselves had IDs, it would be possible to support
// First, make sure there are the right number of models. This code always adds or removes models
// from the end. In the future, if models themselves had IDs, it would be possible to support
Copy link
Contributor

Choose a reason for hiding this comment

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

same, probably still sections instead of models since the diff engine still works on sections

// Since sections don't have IDs, this code keeps the same section cursor on the left and right.
// If sections had IDs this code could be smarter.
// Since models don't have IDs, this code keeps the same section cursor on the left and right.
// If models had IDs this code could be smarter.
Copy link
Contributor

Choose a reason for hiding this comment

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

same

@wkiefer
Copy link
Contributor

wkiefer commented May 30, 2017

Two more thoughts:

  • ModelPath concept is a little odd now, but probably ok for the collection/table bindings.
  • Do we want to land this on a branch to continue work, or as a single PR?

@danielrhammond danielrhammond changed the base branch from master to 0.9-dev June 7, 2017 00:22
@danielrhammond danielrhammond merged commit 1cb1644 into dropbox:0.9-dev Jun 7, 2017
@danielrhammond danielrhammond deleted the flatland branch June 7, 2017 18:05
danielrhammond added a commit that referenced this pull request Jun 10, 2017
Split out sectioning behavior from `ModelCollection` into `SectionedModelCollection` to simplify all the model collection code that only needs to support a single section.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants