├── .bundle └── config ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-version ├── .travis.yml ├── CONTRIBUTING.md ├── Examples ├── Playground.playground │ ├── Pages │ │ └── Usage.xcplaygroundpage │ │ │ └── Contents.swift │ ├── contents.xcplayground │ └── playground.xcworkspace │ │ └── contents.xcworkspacedata ├── SimpleSourceExample.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── SimpleSourceExample.xcscheme ├── SimpleSourceExample.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── SimpleSourceExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── CharacterLoader.swift │ ├── CharacterModel.xcdatamodeld │ └── CharacterModel.xcdatamodel │ │ └── contents │ ├── CharacterTableViewController.swift │ ├── ColorGridCell.swift │ ├── ColorGridHeader.swift │ ├── ColorGridViewController.swift │ ├── ColorLoader.swift │ ├── ColorTableCell.swift │ ├── ColorTableHeader.swift │ ├── ColorTableViewController.swift │ ├── Colors.swift │ ├── CoreDataStack.swift │ ├── CustomHeaderFooterColorTableViewController.swift │ ├── EditableColorTableViewController.swift │ ├── HeaderColorGridViewController.swift │ ├── Info.plist │ ├── ItemListTableViewController.swift │ ├── ReorderingColorGridViewController.swift │ ├── TextHeaderFooterColorTableViewController.swift │ ├── characters.json │ └── colors.json ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── SimpleSource.podspec ├── Sources └── SimpleSource │ ├── BasicData │ ├── BasicDataSource.swift │ └── BasicSection.swift │ ├── CoreData │ ├── CoreDataSource.swift │ └── FetchDelegate.swift │ ├── DataSource.swift │ ├── UICollectionView │ ├── CollectionViewDataSource.swift │ ├── CollectionViewFactory.swift │ ├── CollectionViewReorderingDelegate.swift │ └── UICollectionView+Updates.swift │ ├── UITableView │ ├── TableViewDataSource.swift │ ├── TableViewEditingDelegate.swift │ ├── TableViewFactory.swift │ ├── TableViewReorderingDelegate.swift │ └── UITableView+Updates.swift │ ├── Updates.swift │ └── Utils │ ├── Array+Extensions.swift │ └── Diff.swift ├── Tests └── SimpleSourceTests │ ├── BasicDataSourceTests.swift │ ├── CoreDataSourceTests.swift │ ├── IndexedUpdateHandlerTests.swift │ ├── Resources │ └── TestModel.xcdatamodeld │ │ └── TestModel.xcdatamodel │ │ └── contents │ └── UIKitViewUpdateTests.swift ├── Web ├── SimpleSource-figures.key ├── chart.png └── employee-table.png └── codecov.yml /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "vendor/bundle" 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | 5 | workflow_dispatch: 6 | 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | run_pod_tests: 13 | name: Run pod tests 14 | strategy: 15 | matrix: 16 | os: [macos-12, macos-13, macos-14] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | bundler-cache: true 26 | 27 | - name: Define validation directory 28 | id: define_validation_dir 29 | run: echo "path=${{ runner.temp }}/validation_${{ github.run_number }}_${{ github.run_attempt }}" >> $GITHUB_OUTPUT 30 | 31 | - name: Run pod unit tests 32 | run: | 33 | bundle exec pod lib lint \ 34 | --allow-warnings \ 35 | --validation-dir="${{ steps.define_validation_dir.outputs.path }}" \ 36 | --no-clean 37 | 38 | - name: Upload test artifacts 39 | if: ${{ failure() }} 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: test_artifacts 43 | path: ${{ steps.define_validation_dir.outputs.path }} 44 | 45 | - name: Run SPM unit tests 46 | run: | 47 | xcodebuild test \ 48 | -scheme swift-simple-source \ 49 | -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=latest' \ 50 | | xcbeautify 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | *.DS_Store 3 | 4 | # Xcode 5 | *.pbxuser 6 | *.mode1v3 7 | *.mode2v3 8 | *.perspectivev3 9 | *.xcuserstate 10 | project.xcworkspace/ 11 | xcuserdata/ 12 | *.xccheckout 13 | 14 | #SPM 15 | /.build 16 | /Packages 17 | .swiftpm/ 18 | .netrc 19 | 20 | # RubyGems Bundler 21 | vendor/bundle/ 22 | 23 | # Playgrounds 24 | timeline.xctimeline 25 | 26 | # Generated files 27 | build/ 28 | #*.[oa] 29 | *.pyc 30 | .cache 31 | 32 | # Backup files 33 | *~.nib 34 | *.orig 35 | ~* 36 | 37 | # Localizer script 38 | *.strings.old 39 | *.strings.new 40 | 41 | # Temporary files 42 | *.sqlite-journal 43 | 44 | !Carthage/Build/ 45 | 46 | Pods/ 47 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11.2 2 | language: swift 3 | script: 4 | - pod repo update 5 | - pod install --project-directory=Examples 6 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Examples/SimpleSourceExample.xcworkspace -scheme SimpleSource-Unit-Tests -destination 'name=iPhone 8' ONLY_ACTIVE_ARCH=YES | xcpretty 7 | - pod lib lint --allow-warnings 8 | after_success: 9 | - bash <(curl -s https://codecov.io/bash) -J 'SimpleSource' -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [mheiberg@squarespace.com](mailto:mheiberg@squarespace.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /Examples/Playground.playground/Pages/Usage.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | import PlaygroundSupport 4 | 5 | /*: 6 | - Important: 7 | Make sure to open this playground via `SimpleSourceExample.xcworkspace`. And remember to run `pod install` in the `Examples` directory to install the necessary prerequisites. 8 | */ 9 | /*: 10 | # SimpleSource Basic Usage 11 | 12 | The goal here is to show a bit of data in a table view using SimpleSource. 13 | */ 14 | /*: 15 | ## The Data 16 | 17 | We begin by defining our model object, which must conform to `Equatable`. In this case we'll just use `String`, but you can use anything `Equatable` for the items. 18 | */ 19 | typealias Item = String 20 | /*: 21 | We want to organize our `Item` objects into sections. A section is anything conforming to the `SectionType` protocol. 22 | 23 | A section only has to provide an `items` array. But we are free to add more properties to a section, such as a title (or anything else we need) to properly configure section headers etc. 24 | 25 | To illustrate this, let's also add a title to our custom section type, to make it a little richer. 26 | */ 27 | struct Section: SectionType { 28 | typealias ItemType = Item 29 | var title: String 30 | var items: [ItemType] 31 | } 32 | /*: 33 | - Note: 34 | If you want automatic animated updates when your data mutates, your sections must also 35 | conform to `IdentifiableSection`. That way SimpleSource will be able to tell which sections have changed, 36 | and create the necessary animations for you when the data changes. 37 | 38 | Finally, let's create an array of `Section`s containing some `Item`s. This `[Section]` array will be our data. 39 | */ 40 | let sections: [Section] = [ 41 | Section(title: "First Section", items: ["First item", "Second item"]), 42 | Section(title: "Second Section", items: ["First item in second section"]) 43 | ] 44 | /*: 45 | We are ready to create the data source, which will hold the above data for use in SimpleSource. 46 | 47 | - Note: 48 | For explicit data (basic objects or values arranged in arrays), SimpleSource provides `BasicDataSource`. This is what we will use here. If your data is stored in Core Data you can use `CoreDataSource` instead. 49 | 50 | A `BasicDataSource` exposes a mutable `.sections` property. This can be set to a new value whenever you wish, triggering an update to all views backed by this data source. 51 | */ 52 | let dataSource = BasicDataSource(sections: sections) 53 | /*: 54 | ## The Views 55 | 56 | Let's create a `UIViewController` with a `UITableView` to render our data: 57 | */ 58 | let vc = UITableViewController(style: .grouped) 59 | /*: 60 | To get data into the `UITableView` we will need something which conforms to `UITableViewDataSource`: SimpleSource provides `TableViewDataSource` (to drive table views) and `CollectionViewDataSource` (to drive collection views). 61 | 62 | For our first example here let's create a `TableViewDataSource`. 63 | 64 | Looking at the initializer for `TableViewDataSource` we see that it needs three different things: 65 | 66 | - Some kind of data source from which to get the items 67 | - A view factory from which to get the cells 68 | - A way to incorporate changes in the data into the view 69 | 70 | We already have the data source from before, so the next step is to create a view factory. This will be responsible for emitting cells for the table. 71 | 72 | - Note: 73 | A view factory is initialized with a closure to provide a `reuseIdentifier` for a given item in a given view. Returning different values from this closure based on the item will allow you to mix different cell types in the same view. In our case we only have one cell type, so we only ever return one reuse identifier. 74 | */ 75 | let viewFactory = TableViewFactory { item, view in 76 | return "Cell" 77 | } 78 | /*: 79 | With SimpleSource we use closures to configure cells as they are dequeued for display. 80 | 81 | The convenient thing about these closures is that they are given both a correctly typed cell and a correctly typed model object, which we then use to configure the cell. 82 | 83 | In this simple case we use vanilla `UITableViewCell`s, so that it what the closure gets. But if you have custom cell subclasses then that is what SimpleSource will send to your closure. No need for type casting. 84 | */ 85 | let configureCell = { (cell: UITableViewCell, item: Item, indexPath: IndexPath) -> Void in 86 | cell.textLabel?.text = item 87 | } 88 | /*: 89 | Now we need to register the cell types we want to display with the view factory. 90 | 91 | This teaches the view factory how to dequeue the correct cells from the table view. 92 | 93 | The `registerCell` method takes: 94 | 95 | - A cell instantiation method (you can use nibs, class-based or storyboard prototypes). 96 | - A reuse identifier. 97 | - The view to register the cell in. 98 | - The closure to configure the cell before display. 99 | 100 | - Note: 101 | If you want to mix multiple cells types in your table, there's two steps to it: In the closure passed to the `ViewFactory` initializer, you inspect the item and return the reuse identifier for the cell type you wish to display. Then call `registerCell` on the view factory for each possible reuse identifier that might be emitted from the above closure. 102 | */ 103 | viewFactory.registerCell( 104 | method: .classBased(UITableViewCell.self), 105 | reuseIdentifier: "Cell", 106 | in: vc.tableView, 107 | configuration: configureCell 108 | ) 109 | /*: 110 | For good measure, let's also add some headers to show off the title properties of our sections. 111 | */ 112 | viewFactory.registerHeaderText(in: vc.tableView) { section in 113 | return dataSource.sections[section].title 114 | } 115 | /*: 116 | Now we are ready to create the `UITableViewDataSource` for our table view. This is going to be an instance of `TableViewDataSource`. 117 | 118 | We previously noted that `TableViewDataSource` needs you to provide a way to incorporate changes into the view. Most often you probably want to use one of the built-in row animations for table views, and use `performBatchUpdates` for collection views. 119 | 120 | For table views SimpleSource defines `UITableView.defaultViewUpdate()` which does this animated update for you. If you prefer an unanimated update you can use `UITableView.unanimatedViewUpdate`. Or you can create your own. It's just a closure! You can also pass your favorite `UITableViewRowAnimation` to `defaultViewUpdate()` to customize it. 121 | 122 | For collection views the built-in view updaters are called `UICollectionView.defaultViewUpdate` and `UICollectionView.unanimatedViewUpdate`. 123 | 124 | Time to create the `TableViewDataSource`! 125 | */ 126 | let tableDataSource = TableViewDataSource(dataSource: dataSource, viewFactory: viewFactory, viewUpdate: vc.tableView.defaultViewUpdate()) 127 | /*: 128 | - Important: It is your responsibility to keep a reference to your `TableViewDataSource`. For example in an instance variable of the `UIViewController` using it. This is important because `UITableView` does not retain its `.dataSource` property. – If you want to, you can forget about the `BasicDataSource` and the `TableViewFactory` after you have created your `TableViewDataSource`. They will be retained for as long as they are needed. 129 | 130 | You are free to let one `BasicDataSource` back multiple `TableViewDataSource` or `CollectionViewDataSource` at the same time. All the views will automatically update if you change the data stored in `.sections` (or when the database changes if you are using `CoreDataSource`). 131 | */ 132 | /*: 133 | ## The Result 134 | 135 | Now let's see the table view, rendering our data: 136 | */ 137 | vc.view.bounds.size = CGSize(width: 250, height: 300) 138 | vc.tableView.dataSource = tableDataSource 139 | PlaygroundPage.current.liveView = vc.view 140 | /*: 141 | Press the play button to run this playground (if it's not already running) and open the timeline using Xcode's assistant editor to see the live table view, populated with our data. 142 | */ 143 | /*: 144 | ## What's Next? 145 | 146 | Run the example app in this workspace to see all this in action. Or dig into the documentation provided by the `README.md` file. 147 | */ 148 | -------------------------------------------------------------------------------- /Examples/Playground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Examples/Playground.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample.xcodeproj/xcshareddata/xcschemes/SimpleSourceExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 8 | return true 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/CharacterLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | /// A helper class to load some content into an `NSManagedObjectContext`. 5 | struct CharacterLoader { 6 | static func load(into context: NSManagedObjectContext) { 7 | let dataURL = Bundle.main.url(forResource: "characters", withExtension: "json")! 8 | let data = try! Data(contentsOf: dataURL) 9 | try! JSONDecoder().decode([CharacterRepresentation].self, from: data).forEach { representation in 10 | let character = Character(context: context) 11 | character.name = representation.name 12 | character.race = representation.race 13 | } 14 | } 15 | 16 | private struct CharacterRepresentation: Decodable { 17 | let name: String 18 | let race: String 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/CharacterModel.xcdatamodeld/CharacterModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/CharacterTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This example shows how to drive a table view using Core Data. 5 | /// 6 | /// This is very similar to using static/explicit data arrays. Just replace 7 | /// `BasicDataSource` with `CoreDataSource`. 8 | /// 9 | /// A `CoreDataSource` is initialized with an `NSFetchedResultsController`, which will 10 | /// continually update the connected views if changes are made to the database. 11 | final class CharacterTableViewController: UITableViewController { 12 | private typealias DataSource = CoreDataSource 13 | private typealias ViewFactory = TableViewFactory 14 | 15 | private var coreDataStack: CoreDataStack! 16 | private var tableViewDataSource: TableViewDataSource! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | // Load some data. Normally this would not live in your view controller. 22 | coreDataStack = CoreDataStack() 23 | coreDataStack.loadData() 24 | 25 | configureTableView() 26 | } 27 | 28 | private func configureTableView() { 29 | // The data source needs an `NSFetchedResultsController` when you create it. 30 | // This is how you define what data to show and how to sort and section it. 31 | let dataSource = CoreDataSource(fetchedResultsController: coreDataStack.fetchCharactersByRace()) 32 | let viewFactory = ViewFactory { _,_ in "Cell" } 33 | 34 | // Here we use the built-in `UITableViewCellStyle` for our cells. 35 | viewFactory.registerCell(method: .style(.default), reuseIdentifier: "Cell", in: tableView) { (cell: UITableViewCell, character: Character, indexPath: IndexPath) in 36 | cell.textLabel?.text = character.name 37 | } 38 | 39 | // Show the section name in the headers. 40 | viewFactory.registerHeaderText(in: tableView) { section in 41 | return dataSource.fetchedResultsController.sections?[section].name 42 | } 43 | 44 | tableViewDataSource = TableViewDataSource( 45 | dataSource: dataSource, 46 | viewFactory: viewFactory, 47 | viewUpdate: tableView.defaultViewUpdate()) 48 | 49 | tableView.dataSource = tableViewDataSource 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorGridCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ColorGridCell: UICollectionViewCell { 4 | @IBOutlet fileprivate weak var swatch: UIView! 5 | 6 | static let reuseIdentifier = "ColorGridCell" 7 | 8 | static let configureCell = { (cell: ColorGridCell, item: ColorItem, indexPath: IndexPath) in 9 | cell.swatch.backgroundColor = item.color 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorGridHeader.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | final class ColorGridHeader: UICollectionReusableView { 5 | @IBOutlet fileprivate weak var title: UILabel! 6 | 7 | static let reuseIdentifier = "ColorGridHeader" 8 | 9 | static func configureHeader(dataSource: BasicDataSource) -> (ColorGridHeader, IndexPath) -> Void { 10 | return { (header, indexPath) in 11 | header.title.text = dataSource.sections[indexPath.section].title 12 | } 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorGridViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This example show how to populate a simple collection view. 5 | /// 6 | /// This is very similar to `ColorTableViewController`, except we use the 7 | /// `UICollectionView`-specific classes here: 8 | /// 9 | /// - `CollectionViewFactory` 10 | /// - `CollectionViewDataSource` 11 | /// 12 | /// instead of `TableViewFactory` and `TableViewDataSource`. 13 | /// 14 | /// The underlying data source is exactly the same as for the table view example. 15 | /// It is still a `BasicDataSource`, which knows nothing about the view that will eventually 16 | /// display its data. 17 | final class ColorGridViewController: UICollectionViewController { 18 | private typealias DataSource = BasicDataSource 19 | private typealias ViewFactory = CollectionViewFactory 20 | 21 | private var collectionViewDataSource: CollectionViewDataSource! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | configureCollectionView() 26 | } 27 | 28 | private func configureCollectionView() { 29 | guard let collectionView = self.collectionView else { return } 30 | 31 | let dataSource = DataSource(sections: ColorLoader.loadSections()) 32 | let viewFactory = ViewFactory { _,_ in ColorGridCell.reuseIdentifier } 33 | 34 | viewFactory.registerCell( 35 | method: .dynamic, 36 | reuseIdentifier: ColorGridCell.reuseIdentifier, 37 | in: collectionView, 38 | configuration: ColorGridCell.configureCell) 39 | 40 | collectionViewDataSource = CollectionViewDataSource( 41 | dataSource: dataSource, 42 | viewFactory: viewFactory, 43 | viewUpdate: collectionView.defaultViewUpdate) 44 | 45 | collectionView.dataSource = collectionViewDataSource 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ColorLoader { 4 | 5 | /// Loads colors (organized into sections) from a JSON file. 6 | static func loadSections() -> [ColorSection] { 7 | let dataURL = Bundle.main.url(forResource: "colors", withExtension: "json")! 8 | let data = try! Data(contentsOf: dataURL) 9 | return try! JSONDecoder().decode([ColorSection].self, from: data) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorTableCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ColorTableCell: UITableViewCell { 4 | @IBOutlet fileprivate weak var name: UILabel! 5 | @IBOutlet fileprivate weak var swatch: UIView! 6 | 7 | static let reuseIdentifier = "ColorTableCell" 8 | 9 | static let configureCell = { (cell: ColorTableCell, item: ColorItem, indexPath: IndexPath) in 10 | cell.name.text = item.name 11 | cell.swatch.backgroundColor = item.color 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorTableHeader.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | final class ColorTableHeader: UITableViewHeaderFooterView { 5 | private var titleLabel: UILabel! 6 | 7 | public override init(reuseIdentifier: String?) { 8 | super.init(reuseIdentifier: reuseIdentifier) 9 | setup() 10 | } 11 | 12 | public required init?(coder aDecoder: NSCoder) { 13 | super.init(coder: aDecoder) 14 | setup() 15 | } 16 | 17 | private func setup() { 18 | titleLabel = UILabel() 19 | titleLabel.textAlignment = .center 20 | titleLabel.font = UIFont.boldSystemFont(ofSize: 17) 21 | titleLabel.textColor = .black 22 | titleLabel.autoresizingMask = [.flexibleHeight , .flexibleWidth] 23 | titleLabel.translatesAutoresizingMaskIntoConstraints = true 24 | 25 | contentView.addSubview(titleLabel) 26 | contentView.backgroundColor = UIColor(white: 0.8, alpha: 1) 27 | } 28 | 29 | static func configureHeader(dataSource: BasicDataSource) -> (ColorTableHeader, Int) -> Void { 30 | return { (header, section) in 31 | header.titleLabel.text = dataSource.sections[section].title 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ColorTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This is the most basic example. 5 | /// 6 | /// It shows a list of colors, with no headers/footers and no interactions. 7 | /// 8 | /// The static data comes from a `BasicDataSource`. 9 | final class ColorTableViewController: UITableViewController { 10 | private typealias DataSource = BasicDataSource 11 | private typealias ViewFactory = TableViewFactory 12 | 13 | private var tableViewDataSource: TableViewDataSource! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | configureTableView() 18 | } 19 | 20 | private func configureTableView() { 21 | let dataSource = DataSource(sections: ColorLoader.loadSections()) 22 | let viewFactory = ViewFactory { _,_ in ColorTableCell.reuseIdentifier } 23 | 24 | viewFactory.registerCell( 25 | method: .dynamic, 26 | reuseIdentifier: ColorTableCell.reuseIdentifier, 27 | in: tableView, 28 | configuration: ColorTableCell.configureCell) 29 | 30 | tableViewDataSource = TableViewDataSource( 31 | dataSource: dataSource, 32 | viewFactory: viewFactory, 33 | viewUpdate: tableView.defaultViewUpdate()) 34 | 35 | tableView.dataSource = tableViewDataSource 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/Colors.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SimpleSource 3 | import UIKit 4 | 5 | // MARK: Sections 6 | 7 | struct ColorSection: SectionType { 8 | typealias Item = ColorItem 9 | let title: String 10 | var items: [Item] 11 | } 12 | 13 | extension ColorSection: IdentifiableSection { 14 | var sectionIdentifier: String { return title } 15 | } 16 | 17 | extension ColorSection: Decodable { 18 | enum CodingKeys: String, CodingKey { 19 | case title 20 | case items = "colors" 21 | } 22 | } 23 | 24 | // MARK: Items 25 | 26 | struct ColorItem { 27 | let name: String 28 | let rgb: (CGFloat, CGFloat, CGFloat) 29 | 30 | var color: UIColor { 31 | return UIColor(red: rgb.0, green: rgb.1, blue: rgb.2, alpha: 1) 32 | } 33 | } 34 | 35 | extension ColorItem: Equatable { 36 | static func == (lhs: ColorItem, rhs: ColorItem) -> Bool { 37 | lhs.name == rhs.name && lhs.rgb == rhs.rgb 38 | } 39 | } 40 | 41 | extension ColorItem: Decodable { 42 | enum Errors: Error { 43 | case rgbDecodingFailure 44 | } 45 | enum CodingKeys: String, CodingKey { 46 | case name 47 | case rgb 48 | } 49 | 50 | init(from decoder: Decoder) throws { 51 | let container = try decoder.container(keyedBy: CodingKeys.self) 52 | name = try container.decode(String.self, forKey: .name) 53 | let scanner = try Scanner(string: container.decode(String.self, forKey: .rgb)) 54 | guard 55 | scanner.scanCharacters(from: .init(charactersIn: "#")) == "#", 56 | let rgbUInt64 = scanner.scanUInt64(representation: .hexadecimal) 57 | else { 58 | print("Name: \(name)\nRGBString: \(scanner.string)") 59 | throw Errors.rgbDecodingFailure 60 | } 61 | rgb = ( 62 | CGFloat((rgbUInt64 & 0xff0000) >> 16) / 255, 63 | CGFloat((rgbUInt64 & 0x00ff00) >> 8) / 255, 64 | CGFloat((rgbUInt64 & 0x0000ff) >> 0) / 255 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/CoreDataStack.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | /// A simple, in-memory Core Data stack which can load a list of fictional characters. 5 | final class CoreDataStack { 6 | let managedObjectContext: NSManagedObjectContext 7 | 8 | init() { 9 | let modelURL = Bundle.main.url(forResource: "CharacterModel", withExtension: "momd")! 10 | let model = NSManagedObjectModel(contentsOf: modelURL)! 11 | let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) 12 | try! coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) 13 | managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 14 | managedObjectContext.persistentStoreCoordinator = coordinator 15 | } 16 | 17 | func loadData() { 18 | CharacterLoader.load(into: managedObjectContext) 19 | } 20 | 21 | func fetchCharactersByRace() -> NSFetchedResultsController { 22 | let fetchRequest: NSFetchRequest = Character.fetchRequest() 23 | fetchRequest.sortDescriptors = [ 24 | NSSortDescriptor(key: "race", ascending: true), 25 | NSSortDescriptor(key: "name", ascending: true) 26 | ] 27 | 28 | return NSFetchedResultsController( 29 | fetchRequest: fetchRequest, 30 | managedObjectContext: managedObjectContext, 31 | sectionNameKeyPath: "race", 32 | cacheName: nil) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/CustomHeaderFooterColorTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This example illustrates how to add custom header/footer views to a `UITableView`. 5 | /// 6 | /// `TableViewDataSource` does not implement methods for creating custom header and footer views. 7 | /// That's because they are not the responsibility of a `UITableViewDataSource`. 8 | /// 9 | /// Header and footer text is part of the `UITableViewDataSource` protocol, but custom header/footer 10 | /// views are instead part of the `UITableViewDelegate` protocol. This is arguably a flaw in the design 11 | /// of the `UITableView` API. 12 | /// 13 | /// SimpleSource only aims to be the data source. It does not want to be the delegate for your tables. 14 | /// So if you use custom header and footer views you must implement these methods in your own 15 | /// `UITableViewDelegate` instead. 16 | /// 17 | /// But SimpleSource can still assist you with dequeuing and configuring them! Just register these views 18 | /// with the `TableViewFactory` as normal. Then call the methods on the view factory from your 19 | /// `UITableViewDelegate` to retrieve properly dequeued and configured views. 20 | /// 21 | /// That is the approach shown in this example. 22 | final class CustomHeaderFooterColorTableViewController: UITableViewController { 23 | private typealias DataSource = BasicDataSource 24 | private typealias ViewFactory = TableViewFactory 25 | 26 | private var tableViewDataSource: TableViewDataSource! 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | configureTableView() 31 | } 32 | 33 | private func configureTableView() { 34 | let dataSource = DataSource(sections: ColorLoader.loadSections()) 35 | let viewFactory = ViewFactory { _,_ in ColorTableCell.reuseIdentifier } 36 | 37 | viewFactory.registerCell( 38 | method: .dynamic, 39 | reuseIdentifier: ColorTableCell.reuseIdentifier, 40 | in: tableView, 41 | configuration: ColorTableCell.configureCell) 42 | 43 | // We use a custom class for the header here. That's usually what you want. 44 | // The configuration closure comes from the class itself. 45 | // 46 | // The configuration closure is given the dequeued header/footer view and 47 | // the section number. If you need anything else to properly configure the 48 | // view you are free to capture it in the closure. Here we construct the 49 | // closure in a way that lets it capture the data source, so the section title 50 | // can be retrieved. 51 | viewFactory.registerHeaderFooterView( 52 | method: .classBased(ColorTableHeader.self), 53 | reuseIdentifier: "Header", 54 | in: tableView, 55 | configuration: ColorTableHeader.configureHeader(dataSource: dataSource)) 56 | 57 | // Here we demonstrate a simpler solution for the footer, using a plain view. No nib or subclasses. 58 | // We configure it using a trailing closure, since that's also an option to us. 59 | viewFactory.registerHeaderFooterView( 60 | method: .classBased(UITableViewHeaderFooterView.self), 61 | reuseIdentifier: "Footer", 62 | in: tableView) { (footer: UITableViewHeaderFooterView, section: Int) in 63 | footer.textLabel?.text = "This is the footer in section \(section)." 64 | } 65 | 66 | tableViewDataSource = TableViewDataSource( 67 | dataSource: dataSource, 68 | viewFactory: viewFactory, 69 | viewUpdate: tableView.defaultViewUpdate()) 70 | 71 | tableView.dataSource = tableViewDataSource 72 | } 73 | 74 | // MARK: - UITableViewDelegate 75 | 76 | // Note how we have to implement `UITableViewDelegate`, but we can still use the view factory as a helper to 77 | // create and configure the header and footer views below. 78 | 79 | override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 80 | return tableViewDataSource.viewFactory.headerFooterView(reuseIdentifier: "Header", in: tableView, forSection: section) 81 | } 82 | 83 | override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 84 | return tableViewDataSource.viewFactory.headerFooterView(reuseIdentifier: "Footer", in: tableView, forSection: section) 85 | } 86 | 87 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 88 | return 33 89 | } 90 | 91 | override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 92 | return 44 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/EditableColorTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | fileprivate let configureCell = { (cell: UITableViewCell, item: ColorItem, indexPath: IndexPath) -> Void in 5 | cell.backgroundColor = item.color 6 | } 7 | 8 | /// This example show how you can modify the data stored in a `BasicDataSource` and have 9 | /// the table view respond with automatic animated updates. 10 | /// 11 | /// Tap the + button in the toolbar to add a random color to the data source. 12 | /// 13 | /// Tap a color cell to remove it from the data source. 14 | final class EditableColorTableViewController: UITableViewController { 15 | private typealias DataSource = BasicDataSource 16 | private typealias ViewFactory = TableViewFactory 17 | 18 | // Just a bunch of colors to pick new items from. 19 | private var availableColors = ColorLoader.loadSections().flatMap { $0.items } 20 | 21 | // This is the data source which we will be modifying when the user taps items. 22 | private let dataSource = DataSource(sections: [ColorSection(title: "", items: [])]) 23 | 24 | // The `UITableViewDataSource` object. We must retain it, since `UITableView` does not. 25 | private var tableViewDataSource: TableViewDataSource! 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | configureTableView() 30 | } 31 | 32 | private func configureTableView() { 33 | let viewFactory = ViewFactory { _,_ in "Cell" } 34 | 35 | viewFactory.registerCell( 36 | method: .classBased(UITableViewCell.self), 37 | reuseIdentifier: "Cell", 38 | in: tableView, 39 | configuration: configureCell) 40 | 41 | tableViewDataSource = TableViewDataSource( 42 | dataSource: dataSource, 43 | viewFactory: viewFactory, 44 | viewUpdate: tableView.defaultViewUpdate(with: .automatic)) 45 | 46 | tableView.dataSource = tableViewDataSource 47 | tableView.delegate = self 48 | } 49 | 50 | // MARK: UI callbacks for adding and removing items 51 | 52 | @IBAction func addItem(_ sender: Any) { 53 | addRandomColor() 54 | } 55 | 56 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 57 | removeColor(at: indexPath) 58 | } 59 | 60 | // MARK: Manipulate the data source 61 | 62 | /// Append a random color to the data source. The table view will update automatically. 63 | private func addRandomColor() { 64 | guard !availableColors.isEmpty else { return } 65 | let randomIndex = Int(arc4random_uniform(UInt32(availableColors.count))) 66 | let randomColor = availableColors.remove(at: randomIndex) 67 | dataSource.sections[0].items.append(randomColor) 68 | } 69 | 70 | /// Remove a color from the data source. The table view will update automatically. 71 | private func removeColor(at indexPath: IndexPath) { 72 | let removedColor = dataSource.sections[indexPath.section].items.remove(at: indexPath.item) 73 | availableColors.append(removedColor) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/HeaderColorGridViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This example shows how to add custom headers to a `UICollectionView`. 5 | /// 6 | /// For simplicity we use a standard `UICollectionViewFlowLayout` and add headers. 7 | /// But this approach can also be used to support supplementary view for any custom 8 | /// `UICollectionViewLayout` you may have. 9 | /// 10 | /// Just use your own custom `kind` values defined by the layout when you call 11 | /// `registerSupplementaryView` on the view factory. 12 | final class HeaderColorGridViewController: UICollectionViewController { 13 | private typealias DataSource = BasicDataSource 14 | private typealias ViewFactory = CollectionViewFactory 15 | 16 | private var collectionViewDataSource: CollectionViewDataSource! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | configureCollectionView() 21 | } 22 | 23 | private func configureCollectionView() { 24 | guard let collectionView = self.collectionView else { return } 25 | 26 | // Let our custom headers pin to the top when scrolling. 27 | if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { 28 | flowLayout.sectionHeadersPinToVisibleBounds = true 29 | } 30 | 31 | let reuseIdentifier = "Cell" 32 | let dataSource = DataSource(sections: ColorLoader.loadSections()) 33 | let viewFactory = ViewFactory { _,_ in reuseIdentifier } 34 | 35 | // Here we use a trailing closure to configure the cells. Just to try something different. 36 | // We don't even bother with a custom cell class, and the reuse identifier could be anything, 37 | // as long as it matches the value returned from the closure given to ViewFactory above. 38 | viewFactory.registerCell( 39 | method: .classBased(UICollectionViewCell.self), 40 | reuseIdentifier: reuseIdentifier, 41 | in: collectionView) { (cell: UICollectionViewCell, item: ColorItem, indexPath: IndexPath) in 42 | cell.contentView.backgroundColor = item.color 43 | cell.contentView.layer.cornerRadius = 6 44 | } 45 | 46 | // UICollectionViewFlowLayout uses UICollectionElementKindSectionHeader for the supplementary view kind. 47 | // 48 | // Note that the configuration closure for a supplementary view does not get an item passed in. 49 | // 50 | // The reason is that an item may not exist in the data source for the index path. One example could be 51 | // headers for empty sections. The header has an index path, but no item exists for that index path. 52 | // 53 | // Some supplementary views need the item. E.g. title labels under cells. Others (like section headers 54 | // and footers) need to access section information which is only available through the data source. 55 | // And some might not even need the data source because the views are independant of the data. 56 | // 57 | // In our case we need access to the section title, so we construct the closure using the data source. 58 | // You are free to construct configuration closures that capture whatever you need to set up the 59 | // supplementary views. 60 | let configureHeader = ColorGridHeader.configureHeader(dataSource: dataSource) 61 | viewFactory.registerSupplementaryView( 62 | method: .dynamic, 63 | kind: UICollectionView.elementKindSectionHeader, 64 | reuseIdentifier: ColorGridHeader.reuseIdentifier, 65 | in: collectionView, 66 | configuration: configureHeader) 67 | 68 | collectionViewDataSource = CollectionViewDataSource( 69 | dataSource: dataSource, 70 | viewFactory: viewFactory, 71 | viewUpdate: collectionView.defaultViewUpdate) 72 | 73 | collectionView.dataSource = collectionViewDataSource 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ItemListTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | // MARK: - ListItem 5 | 6 | private struct ListItem { 7 | let uuid: String 8 | let name: String 9 | 10 | init(name: String) { 11 | self.uuid = UUID().uuidString 12 | self.name = name 13 | } 14 | } 15 | 16 | extension ListItem: IdentifiableItem { 17 | var itemIdentifier: String { 18 | return uuid 19 | } 20 | } 21 | 22 | extension ListItem: Equatable { 23 | static func ==(lhs: ListItem, rhs: ListItem) -> Bool { 24 | return (lhs.uuid == rhs.uuid) && (lhs.name == rhs.name) 25 | } 26 | } 27 | 28 | // MARK: - View Controller 29 | 30 | fileprivate let configureCell = { (cell: UITableViewCell, item: ListItem, indexPath: IndexPath) -> Void in 31 | cell.textLabel?.text = item.name 32 | } 33 | 34 | final class ItemListTableViewController: UITableViewController { 35 | private typealias Section = BasicIdentifiableSection 36 | private typealias DataSource = BasicDataSource
37 | private typealias ViewFactory = TableViewFactory 38 | 39 | private var itemCounter = 0 40 | private let dataSource = DataSource(sections: [Section(sectionIdentifier: "The only section", items: [])]) 41 | 42 | // The `UITableViewDataSource` object. We must retain it, since `UITableView` does not. 43 | private var tableViewDataSource: TableViewDataSource! 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | configureTableView() 48 | } 49 | 50 | // MARK: Table View 51 | 52 | private func configureTableView() { 53 | let viewFactory = ViewFactory { _,_ in "Cell" } 54 | 55 | viewFactory.registerCell( 56 | method: .style(.default), 57 | reuseIdentifier: "Cell", 58 | in: tableView, 59 | configuration: configureCell) 60 | 61 | tableViewDataSource = TableViewDataSource( 62 | dataSource: dataSource, 63 | viewFactory: viewFactory, 64 | viewUpdate: tableView.defaultViewUpdate(with: .automatic)) 65 | 66 | // Hook into the data source to handle editing and reordering. 67 | tableViewDataSource.editingDelegate = self 68 | tableViewDataSource.reorderingDelegate = self 69 | 70 | tableView.dataSource = tableViewDataSource 71 | tableView.delegate = self 72 | } 73 | 74 | // Prevent regular cell selection. 75 | override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 76 | return false 77 | } 78 | 79 | // This method is part of UITableViewDelegate, so it will not be provided or proxied by the SimpleSource data source. 80 | // SimpleSource only implements methods from UITableViewDataSource. 81 | override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { 82 | return .delete 83 | } 84 | 85 | // MARK: Navigation bar buttons 86 | 87 | @IBAction func addItem(_ sender: Any) { 88 | let newItem = ListItem(name: "Item \(itemCounter)") 89 | dataSource.sections[0].items.append(newItem) 90 | itemCounter += 1 91 | } 92 | 93 | @IBAction func toggleEditMode(_ sender: Any) { 94 | tableView.setEditing(!tableView.isEditing, animated: true) 95 | } 96 | } 97 | 98 | // MARK: Editing 99 | 100 | extension ItemListTableViewController: TableViewEditingDelegate { 101 | func editing(tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 102 | dataSource.sections[0].items.remove(at: indexPath.item) 103 | } 104 | } 105 | 106 | // MARK: Reordering 107 | 108 | extension ItemListTableViewController: TableViewReorderingDelegate { 109 | func reordering(tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 110 | dataSource.moveItem(at: sourceIndexPath, to: destinationIndexPath) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/ReorderingColorGridViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This example shows how to reorder cells in a collection view. 5 | /// 6 | /// Setting a `reorderingDelegate` on the `CollectionViewDataSource` indicates that 7 | /// reordering is supported. Implementing this delegate for a `BasicDataSource` is a 8 | /// simple one-liner. 9 | /// 10 | /// In this example we rely on the reordering gestures automatically installed by 11 | /// `UICollectionViewController` (see the documentation for `installsStandardGestureForInteractiveMovement`). 12 | /// 13 | /// If you're not using `UICollectionViewController` you can install your own gestures and 14 | /// call the relevant reordering methods on the collection view as touches are processed. 15 | /// But that's not specific to SimpleSource. There is an example of how to do this at 16 | /// http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/ 17 | final class ReorderingColorGridViewController: UICollectionViewController { 18 | fileprivate typealias DataSource = BasicDataSource 19 | fileprivate typealias ViewFactory = CollectionViewFactory 20 | 21 | fileprivate var collectionViewDataSource: CollectionViewDataSource! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | configureCollectionView() 26 | } 27 | 28 | fileprivate func configureCollectionView() { 29 | guard let collectionView = self.collectionView else { return } 30 | 31 | let dataSource = DataSource(sections: ColorLoader.loadSections()) 32 | let viewFactory = ViewFactory { _,_ in ColorGridCell.reuseIdentifier } 33 | 34 | viewFactory.registerCell( 35 | method: .dynamic, 36 | reuseIdentifier: ColorGridCell.reuseIdentifier, 37 | in: collectionView, 38 | configuration: ColorGridCell.configureCell) 39 | 40 | collectionViewDataSource = CollectionViewDataSource( 41 | dataSource: dataSource, 42 | viewFactory: viewFactory, 43 | viewUpdate: collectionView.defaultViewUpdate) 44 | 45 | collectionViewDataSource.reorderingDelegate = self 46 | 47 | collectionView.dataSource = collectionViewDataSource 48 | } 49 | 50 | } 51 | 52 | // The `BasicDataSource` knows how to move items around, so this implementation is easy. 53 | // 54 | // If you use a `CoreDataSource` you need to modify your managed objects in a way that makes the 55 | // item move as specified in the `NSFetchedResultsController`, and then save the context. This 56 | // depends on your Core Data models and their sort criteria. 57 | // 58 | // Any changes must be made synchronously, before you return from this method. Otherwise the 59 | // change to the data source will be picked up as a normal modification and sent to the `viewUpdate` 60 | // which will try to update the collection view (which is already updated by the reordering). 61 | extension ReorderingColorGridViewController: CollectionViewReorderingDelegate { 62 | func reordering(collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 63 | collectionViewDataSource.dataSource.moveItem(at: sourceIndexPath, to: destinationIndexPath) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/TextHeaderFooterColorTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SimpleSource 3 | 4 | /// This example shows how to add header and footer text to a `UITableView`. 5 | final class TextHeaderFooterColorTableViewController: UITableViewController { 6 | private typealias DataSource = BasicDataSource 7 | private typealias ViewFactory = TableViewFactory 8 | 9 | private var tableViewDataSource: TableViewDataSource! 10 | 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | configureTableView() 14 | } 15 | 16 | private func configureTableView() { 17 | let dataSource = DataSource(sections: ColorLoader.loadSections()) 18 | let viewFactory = ViewFactory { _,_ in ColorTableCell.reuseIdentifier } 19 | 20 | viewFactory.registerCell( 21 | method: .dynamic, 22 | reuseIdentifier: ColorTableCell.reuseIdentifier, 23 | in: tableView, 24 | configuration: ColorTableCell.configureCell) 25 | 26 | viewFactory.registerHeaderText(in: tableView) { section in 27 | return dataSource.sections[section].title 28 | } 29 | 30 | viewFactory.registerFooterText(in: tableView) { section in 31 | let itemCount = dataSource.sections[section].items.count 32 | let itemString = (itemCount == 1) ? "item" : "items" 33 | return "\(itemCount) \(itemString)" 34 | } 35 | 36 | tableViewDataSource = TableViewDataSource( 37 | dataSource: dataSource, 38 | viewFactory: viewFactory, 39 | viewUpdate: tableView.defaultViewUpdate()) 40 | 41 | tableView.dataSource = tableViewDataSource 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/characters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "Bilbo", "race": "Hobbit"}, 3 | { "name": "Frodo", "race": "Hobbit"}, 4 | { "name": "Sméagol", "race": "Hobbit"}, 5 | { "name": "Déagol", "race": "Hobbit"}, 6 | { "name": "Samwise", "race": "Hobbit"}, 7 | { "name": "Meriadoc", "race": "Hobbit"}, 8 | { "name": "Peregrin", "race": "Hobbit"}, 9 | 10 | { "name": "Gandalf", "race": "Human" }, 11 | { "name": "Aragorn", "race": "Human" }, 12 | { "name": "Boromir", "race": "Human" }, 13 | { "name": "Théoden", "race": "Human" }, 14 | { "name": "Grìma Wormtongue", "race": "Human" }, 15 | { "name": "Faramir", "race": "Human" }, 16 | { "name": "Denethor", "race": "Human" }, 17 | 18 | { "name": "Arwen Evenstar", "race": "Elf" }, 19 | { "name": "Galadriel", "race": "Elf" }, 20 | { "name": "Elrond", "race": "Elf" }, 21 | { "name": "Glorfindel", "race": "Elf" }, 22 | 23 | { "name": "Fíli", "race": "Dwarf" }, 24 | { "name": "Kíli", "race": "Dwarf" }, 25 | { "name": "Óin", "race": "Dwarf" }, 26 | { "name": "Glóin", "race": "Dwarf" }, 27 | { "name": "Balin", "race": "Dwarf" }, 28 | { "name": "Dwalin", "race": "Dwarf" }, 29 | { "name": "Ori", "race": "Dwarf" }, 30 | { "name": "Dori", "race": "Dwarf" }, 31 | { "name": "Nori", "race": "Dwarf" }, 32 | { "name": "Bifur", "race": "Dwarf" }, 33 | { "name": "Bofur", "race": "Dwarf" }, 34 | { "name": "Bombur", "race": "Dwarf" } 35 | ] 36 | -------------------------------------------------------------------------------- /Examples/SimpleSourceExample/colors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Blacks", 4 | "colors": [ 5 | { 6 | "rgb": "#000000", 7 | "name": "Black" 8 | }, 9 | { 10 | "rgb": "#0C090A", 11 | "name": "Night" 12 | }, 13 | { 14 | "rgb": "#2C3539", 15 | "name": "Gunmetal" 16 | }, 17 | { 18 | "rgb": "#2B1B17", 19 | "name": "Midnight" 20 | }, 21 | { 22 | "rgb": "#34282C", 23 | "name": "Charcoal" 24 | }, 25 | { 26 | "rgb": "#25383C", 27 | "name": "Dark Slate Grey" 28 | }, 29 | { 30 | "rgb": "#3B3131", 31 | "name": "Oil" 32 | }, 33 | { 34 | "rgb": "#413839", 35 | "name": "Black Cat" 36 | }, 37 | { 38 | "rgb": "#3D3C3A", 39 | "name": "Iridium" 40 | }, 41 | { 42 | "rgb": "#463E3F", 43 | "name": "Black Eel" 44 | }, 45 | { 46 | "rgb": "#4C4646", 47 | "name": "Black Cow" 48 | } 49 | ] 50 | }, 51 | { 52 | "title": "Grays", 53 | "colors": [ 54 | { 55 | "rgb": "#504A4B", 56 | "name": "Gray Wolf" 57 | }, 58 | { 59 | "rgb": "#565051", 60 | "name": "Vampire Gray" 61 | }, 62 | { 63 | "rgb": "#5C5858", 64 | "name": "Gray Dolphin" 65 | }, 66 | { 67 | "rgb": "#625D5D", 68 | "name": "Carbon Gray" 69 | }, 70 | { 71 | "rgb": "#666362", 72 | "name": "Ash Gray" 73 | }, 74 | { 75 | "rgb": "#6D6968", 76 | "name": "Cloudy Gray" 77 | }, 78 | { 79 | "rgb": "#726E6D", 80 | "name": "Smokey Gray" 81 | }, 82 | { 83 | "rgb": "#736F6E", 84 | "name": "Gray" 85 | }, 86 | { 87 | "rgb": "#837E7C", 88 | "name": "Granite" 89 | }, 90 | { 91 | "rgb": "#848482", 92 | "name": "Battleship Gray" 93 | }, 94 | { 95 | "rgb": "#B6B6B4", 96 | "name": "Gray Cloud" 97 | }, 98 | { 99 | "rgb": "#D1D0CE", 100 | "name": "Gray Goose" 101 | }, 102 | { 103 | "rgb": "#E5E4E2", 104 | "name": "Platinum" 105 | }, 106 | { 107 | "rgb": "#BCC6CC", 108 | "name": "Metallic Silver" 109 | } 110 | ] 111 | }, 112 | { 113 | "title": "Blues", 114 | "colors": [ 115 | { 116 | "rgb": "#98AFC7", 117 | "name": "Blue Gray" 118 | }, 119 | { 120 | "rgb": "#6D7B8D", 121 | "name": "Light Slate Gray" 122 | }, 123 | { 124 | "rgb": "#657383", 125 | "name": "Slate Gray" 126 | }, 127 | { 128 | "rgb": "#616D7E", 129 | "name": "Jet Gray" 130 | }, 131 | { 132 | "rgb": "#646D7E", 133 | "name": "Mist Blue" 134 | }, 135 | { 136 | "rgb": "#566D7E", 137 | "name": "Marble Blue" 138 | }, 139 | { 140 | "rgb": "#737CA1", 141 | "name": "Slate Blue" 142 | }, 143 | { 144 | "rgb": "#4863A0", 145 | "name": "Steel Blue" 146 | }, 147 | { 148 | "rgb": "#2B547E", 149 | "name": "Blue Jay" 150 | }, 151 | { 152 | "rgb": "#2B3856", 153 | "name": "Dark Slate Blue" 154 | }, 155 | { 156 | "rgb": "#151B54", 157 | "name": "Midnight Blue" 158 | }, 159 | { 160 | "rgb": "#000080", 161 | "name": "Navy Blue" 162 | }, 163 | { 164 | "rgb": "#342D7E", 165 | "name": "Blue Whale" 166 | }, 167 | { 168 | "rgb": "#15317E", 169 | "name": "Lapis Blue" 170 | }, 171 | { 172 | "rgb": "#151B8D", 173 | "name": "Denim Dark Blue" 174 | }, 175 | { 176 | "rgb": "#0000A0", 177 | "name": "Earth Blue" 178 | }, 179 | { 180 | "rgb": "#0020C2", 181 | "name": "Cobalt Blue" 182 | }, 183 | { 184 | "rgb": "#0041C2", 185 | "name": "Blueberry Blue" 186 | }, 187 | { 188 | "rgb": "#2554C7", 189 | "name": "Sapphire Blue" 190 | }, 191 | { 192 | "rgb": "#1569C7", 193 | "name": "Blue Eyes" 194 | }, 195 | { 196 | "rgb": "#2B60DE", 197 | "name": "Royal Blue" 198 | }, 199 | { 200 | "rgb": "#1F45FC", 201 | "name": "Blue Orchid" 202 | }, 203 | { 204 | "rgb": "#6960EC", 205 | "name": "Blue Lotus" 206 | }, 207 | { 208 | "rgb": "#736AFF", 209 | "name": "Light Slate Blue" 210 | }, 211 | { 212 | "rgb": "#357EC7", 213 | "name": "Windows Blue" 214 | }, 215 | { 216 | "rgb": "#368BC1", 217 | "name": "Glacial Blue Ice" 218 | }, 219 | { 220 | "rgb": "#488AC7", 221 | "name": "Silk Blue" 222 | }, 223 | { 224 | "rgb": "#3090C7", 225 | "name": "Blue Ivy" 226 | }, 227 | { 228 | "rgb": "#659EC7", 229 | "name": "Blue Koi" 230 | }, 231 | { 232 | "rgb": "#B7CEEC", 233 | "name": "Blue Angel" 234 | }, 235 | { 236 | "rgb": "#B4CFEC", 237 | "name": "Pastel Blue" 238 | }, 239 | { 240 | "rgb": "#C2DFFF", 241 | "name": "Sea Blue" 242 | }, 243 | { 244 | "rgb": "#C6DEFF", 245 | "name": "Powder Blue" 246 | }, 247 | { 248 | "rgb": "#AFDCEC", 249 | "name": "Coral Blue" 250 | }, 251 | { 252 | "rgb": "#ADDFFF", 253 | "name": "Light Blue" 254 | }, 255 | { 256 | "rgb": "#BDEDFF", 257 | "name": "Robin Egg Blue" 258 | }, 259 | { 260 | "rgb": "#CFECEC", 261 | "name": "Pale Blue Lily" 262 | }, 263 | { 264 | "rgb": "#E0FFFF", 265 | "name": "Light Cyan" 266 | }, 267 | { 268 | "rgb": "#EBF4FA", 269 | "name": "Water" 270 | }, 271 | { 272 | "rgb": "#F0F8FF", 273 | "name": "AliceBlue" 274 | }, 275 | { 276 | "rgb": "#F0FFFF", 277 | "name": "Azure" 278 | }, 279 | { 280 | "rgb": "#CCFFFF", 281 | "name": "Light Slate" 282 | }, 283 | { 284 | "rgb": "#93FFE8", 285 | "name": "Light Aquamarine" 286 | }, 287 | { 288 | "rgb": "#9AFEFF", 289 | "name": "Electric Blue" 290 | }, 291 | { 292 | "rgb": "#7FFFD4", 293 | "name": "Aquamarine" 294 | }, 295 | { 296 | "rgb": "#00FFFF", 297 | "name": "Cyan or Aqua" 298 | }, 299 | { 300 | "rgb": "#7DFDFE", 301 | "name": "Tron Blue" 302 | }, 303 | { 304 | "rgb": "#57FEFF", 305 | "name": "Blue Zircon" 306 | }, 307 | { 308 | "rgb": "#8EEBEC", 309 | "name": "Blue Lagoon" 310 | }, 311 | { 312 | "rgb": "#50EBEC", 313 | "name": "Celeste" 314 | }, 315 | { 316 | "rgb": "#4EE2EC", 317 | "name": "Blue Diamond" 318 | }, 319 | { 320 | "rgb": "#81D8D0", 321 | "name": "Tiffany Blue" 322 | }, 323 | { 324 | "rgb": "#92C7C7", 325 | "name": "Cyan Opaque" 326 | }, 327 | { 328 | "rgb": "#77BFC7", 329 | "name": "Blue Hosta" 330 | }, 331 | { 332 | "rgb": "#78C7C7", 333 | "name": "Northern Lights Blue" 334 | }, 335 | { 336 | "rgb": "#48CCCD", 337 | "name": "Medium Turquoise" 338 | }, 339 | { 340 | "rgb": "#43C6DB", 341 | "name": "Turquoise" 342 | }, 343 | { 344 | "rgb": "#46C7C7", 345 | "name": "Jellyfish" 346 | } 347 | ] 348 | }, 349 | { 350 | "title": "Greens", 351 | "colors": [ 352 | { 353 | "rgb": "#7BCCB5", 354 | "name": "Blue green" 355 | }, 356 | { 357 | "rgb": "#43BFC7", 358 | "name": "Macaw Blue Green" 359 | }, 360 | { 361 | "rgb": "#3EA99F", 362 | "name": "Light Sea Green" 363 | }, 364 | { 365 | "rgb": "#3B9C9C", 366 | "name": "Dark Turquoise" 367 | }, 368 | { 369 | "rgb": "#438D80", 370 | "name": "Sea Turtle Green" 371 | }, 372 | { 373 | "rgb": "#348781", 374 | "name": "Medium Aquamarine" 375 | }, 376 | { 377 | "rgb": "#307D7E", 378 | "name": "Greenish Blue" 379 | }, 380 | { 381 | "rgb": "#5E7D7E", 382 | "name": "Grayish Turquoise" 383 | }, 384 | { 385 | "rgb": "#4C787E", 386 | "name": "Beetle Green" 387 | }, 388 | { 389 | "rgb": "#008080", 390 | "name": "Teal" 391 | }, 392 | { 393 | "rgb": "#4E8975", 394 | "name": "Sea Green" 395 | }, 396 | { 397 | "rgb": "#78866B", 398 | "name": "Camouflage Green" 399 | }, 400 | { 401 | "rgb": "#848b79", 402 | "name": "Sage Green" 403 | }, 404 | { 405 | "rgb": "#617C58", 406 | "name": "Hazel Green" 407 | }, 408 | { 409 | "rgb": "#728C00", 410 | "name": "Venom Green" 411 | }, 412 | { 413 | "rgb": "#667C26", 414 | "name": "Fern Green" 415 | }, 416 | { 417 | "rgb": "#254117", 418 | "name": "Dark Forest Green" 419 | }, 420 | { 421 | "rgb": "#306754", 422 | "name": "Medium Sea Green" 423 | }, 424 | { 425 | "rgb": "#347235", 426 | "name": "Medium Forest Green" 427 | }, 428 | { 429 | "rgb": "#437C17", 430 | "name": "Seaweed Green" 431 | }, 432 | { 433 | "rgb": "#387C44", 434 | "name": "Pine Green" 435 | }, 436 | { 437 | "rgb": "#347C2C", 438 | "name": "Jungle Green" 439 | }, 440 | { 441 | "rgb": "#347C17", 442 | "name": "Shamrock Green" 443 | }, 444 | { 445 | "rgb": "#348017", 446 | "name": "Medium Spring Green" 447 | }, 448 | { 449 | "rgb": "#4E9258", 450 | "name": "Forest Green" 451 | }, 452 | { 453 | "rgb": "#6AA121", 454 | "name": "Green Onion" 455 | }, 456 | { 457 | "rgb": "#4AA02C", 458 | "name": "Spring Green" 459 | }, 460 | { 461 | "rgb": "#41A317", 462 | "name": "Lime Green" 463 | }, 464 | { 465 | "rgb": "#3EA055", 466 | "name": "Clover Green" 467 | }, 468 | { 469 | "rgb": "#6CBB3C", 470 | "name": "Green Snake" 471 | }, 472 | { 473 | "rgb": "#6CC417", 474 | "name": "Alien Green" 475 | }, 476 | { 477 | "rgb": "#4CC417", 478 | "name": "Green Apple" 479 | }, 480 | { 481 | "rgb": "#52D017", 482 | "name": "Yellow Green" 483 | }, 484 | { 485 | "rgb": "#4CC552", 486 | "name": "Kelly Green" 487 | }, 488 | { 489 | "rgb": "#54C571", 490 | "name": "Zombie Green" 491 | }, 492 | { 493 | "rgb": "#99C68E", 494 | "name": "Frog Green" 495 | }, 496 | { 497 | "rgb": "#89C35C", 498 | "name": "Green Peas" 499 | }, 500 | { 501 | "rgb": "#85BB65", 502 | "name": "Dollar Bill Green" 503 | }, 504 | { 505 | "rgb": "#8BB381", 506 | "name": "Dark Sea Green" 507 | }, 508 | { 509 | "rgb": "#9CB071", 510 | "name": "Iguana Green" 511 | }, 512 | { 513 | "rgb": "#B2C248", 514 | "name": "Avocado Green" 515 | }, 516 | { 517 | "rgb": "#9DC209", 518 | "name": "Pistachio Green" 519 | }, 520 | { 521 | "rgb": "#A1C935", 522 | "name": "Salad Green" 523 | }, 524 | { 525 | "rgb": "#7FE817", 526 | "name": "Hummingbird Green" 527 | }, 528 | { 529 | "rgb": "#59E817", 530 | "name": "Nebula Green" 531 | }, 532 | { 533 | "rgb": "#B5EAAA", 534 | "name": "Green Thumb" 535 | }, 536 | { 537 | "rgb": "#C3FDB8", 538 | "name": "Light Jade" 539 | }, 540 | { 541 | "rgb": "#CCFB5D", 542 | "name": "Tea Green" 543 | }, 544 | { 545 | "rgb": "#B1FB17", 546 | "name": "Green Yellow" 547 | }, 548 | { 549 | "rgb": "#BCE954", 550 | "name": "Slime Green" 551 | }, 552 | { 553 | "rgb": "#EDDA74", 554 | "name": "Goldenrod" 555 | }, 556 | { 557 | "rgb": "#EDE275", 558 | "name": "Harvest Gold" 559 | } 560 | ] 561 | }, 562 | { 563 | "title": "Yellows", 564 | "colors": [ 565 | { 566 | "rgb": "#FFE87C", 567 | "name": "Sun Yellow" 568 | }, 569 | { 570 | "rgb": "#FFFF00", 571 | "name": "Yellow" 572 | }, 573 | { 574 | "rgb": "#FFF380", 575 | "name": "Corn Yellow" 576 | }, 577 | { 578 | "rgb": "#FFFFC2", 579 | "name": "Parchment" 580 | }, 581 | { 582 | "rgb": "#FFFFCC", 583 | "name": "Cream" 584 | }, 585 | { 586 | "rgb": "#FFF8C6", 587 | "name": "Lemon Chiffon" 588 | }, 589 | { 590 | "rgb": "#FFF8DC", 591 | "name": "Cornsilk" 592 | }, 593 | { 594 | "rgb": "#F5F5DC", 595 | "name": "Beige" 596 | }, 597 | { 598 | "rgb": "#FBF6D9", 599 | "name": "Blonde" 600 | }, 601 | { 602 | "rgb": "#FAEBD7", 603 | "name": "AntiqueWhite" 604 | }, 605 | { 606 | "rgb": "#F7E7CE", 607 | "name": "Champagne" 608 | }, 609 | { 610 | "rgb": "#FFEBCD", 611 | "name": "BlanchedAlmond" 612 | }, 613 | { 614 | "rgb": "#F3E5AB", 615 | "name": "Vanilla" 616 | }, 617 | { 618 | "rgb": "#ECE5B6", 619 | "name": "Tan Brown" 620 | }, 621 | { 622 | "rgb": "#FFE5B4", 623 | "name": "Peach" 624 | }, 625 | { 626 | "rgb": "#FFDB58", 627 | "name": "Mustard" 628 | }, 629 | { 630 | "rgb": "#FFD801", 631 | "name": "Rubber Ducky Yellow" 632 | }, 633 | { 634 | "rgb": "#FDD017", 635 | "name": "Bright Gold" 636 | } 637 | ] 638 | }, 639 | { 640 | "title": "Browns", 641 | "colors": [ 642 | { 643 | "rgb": "#EAC117", 644 | "name": "Golden brown" 645 | }, 646 | { 647 | "rgb": "#F2BB66", 648 | "name": "Macaroni and Cheese" 649 | }, 650 | { 651 | "rgb": "#FBB917", 652 | "name": "Saffron" 653 | }, 654 | { 655 | "rgb": "#FBB117", 656 | "name": "Beer" 657 | }, 658 | { 659 | "rgb": "#FFA62F", 660 | "name": "Cantaloupe" 661 | }, 662 | { 663 | "rgb": "#E9AB17", 664 | "name": "Bee Yellow" 665 | }, 666 | { 667 | "rgb": "#E2A76F", 668 | "name": "Brown Sugar" 669 | }, 670 | { 671 | "rgb": "#DEB887", 672 | "name": "BurlyWood" 673 | }, 674 | { 675 | "rgb": "#FFCBA4", 676 | "name": "Deep Peach" 677 | }, 678 | { 679 | "rgb": "#C68E17", 680 | "name": "Caramel" 681 | }, 682 | { 683 | "rgb": "#B5A642", 684 | "name": "Brass" 685 | }, 686 | { 687 | "rgb": "#ADA96E", 688 | "name": "Khaki" 689 | }, 690 | { 691 | "rgb": "#C19A6B", 692 | "name": "Camel brown" 693 | }, 694 | { 695 | "rgb": "#CD7F32", 696 | "name": "Bronze" 697 | }, 698 | { 699 | "rgb": "#C88141", 700 | "name": "Tiger Orange" 701 | }, 702 | { 703 | "rgb": "#C58917", 704 | "name": "Cinnamon" 705 | }, 706 | { 707 | "rgb": "#AF9B60", 708 | "name": "Bullet Shell" 709 | }, 710 | { 711 | "rgb": "#493D26", 712 | "name": "Mocha" 713 | }, 714 | { 715 | "rgb": "#483C32", 716 | "name": "Taupe" 717 | }, 718 | { 719 | "rgb": "#6F4E37", 720 | "name": "Coffee" 721 | }, 722 | { 723 | "rgb": "#835C3B", 724 | "name": "Brown Bear" 725 | }, 726 | { 727 | "rgb": "#7F5217", 728 | "name": "Red Dirt" 729 | }, 730 | { 731 | "rgb": "#7F462C", 732 | "name": "Sepia" 733 | } 734 | ] 735 | }, 736 | { 737 | "title": "Orange", 738 | "colors": [ 739 | { 740 | "rgb": "#C47451", 741 | "name": "Orange Salmon" 742 | }, 743 | { 744 | "rgb": "#C36241", 745 | "name": "Rust" 746 | }, 747 | { 748 | "rgb": "#C35817", 749 | "name": "Red Fox" 750 | }, 751 | { 752 | "rgb": "#C85A17", 753 | "name": "Chocolate" 754 | }, 755 | { 756 | "rgb": "#CC6600", 757 | "name": "Sedona" 758 | }, 759 | { 760 | "rgb": "#E56717", 761 | "name": "Papaya Orange" 762 | }, 763 | { 764 | "rgb": "#E66C2C", 765 | "name": "Halloween Orange" 766 | }, 767 | { 768 | "rgb": "#F87217", 769 | "name": "Pumpkin Orange" 770 | }, 771 | { 772 | "rgb": "#E78A61", 773 | "name": "Tangerine" 774 | }, 775 | { 776 | "rgb": "#E18B6B", 777 | "name": "Dark Salmon" 778 | }, 779 | { 780 | "rgb": "#E77471", 781 | "name": "Light Coral" 782 | }, 783 | { 784 | "rgb": "#F75D59", 785 | "name": "Bean Red" 786 | }, 787 | { 788 | "rgb": "#E55451", 789 | "name": "Valentine Red" 790 | }, 791 | { 792 | "rgb": "#E55B3C", 793 | "name": "Shocking Orange" 794 | } 795 | ] 796 | }, 797 | { 798 | "title": "Reds", 799 | "colors": [ 800 | { 801 | "rgb": "#FF0000", 802 | "name": "Red" 803 | }, 804 | { 805 | "rgb": "#FF2400", 806 | "name": "Scarlet" 807 | }, 808 | { 809 | "rgb": "#F62217", 810 | "name": "Ruby Red" 811 | }, 812 | { 813 | "rgb": "#F70D1A", 814 | "name": "Ferrari Red" 815 | }, 816 | { 817 | "rgb": "#F62817", 818 | "name": "Fire Engine Red" 819 | }, 820 | { 821 | "rgb": "#E42217", 822 | "name": "Lava Red" 823 | }, 824 | { 825 | "rgb": "#E41B17", 826 | "name": "Love Red" 827 | }, 828 | { 829 | "rgb": "#DC381F", 830 | "name": "Grapefruit" 831 | }, 832 | { 833 | "rgb": "#C34A2C", 834 | "name": "Chestnut Red" 835 | }, 836 | { 837 | "rgb": "#7E3517", 838 | "name": "Blood Red" 839 | }, 840 | { 841 | "rgb": "#8A4117", 842 | "name": "Sienna" 843 | }, 844 | { 845 | "rgb": "#7E3817", 846 | "name": "Sangria" 847 | }, 848 | { 849 | "rgb": "#800517", 850 | "name": "Firebrick" 851 | }, 852 | { 853 | "rgb": "#810541", 854 | "name": "Maroon" 855 | }, 856 | { 857 | "rgb": "#7D0541", 858 | "name": "Plum Pie" 859 | }, 860 | { 861 | "rgb": "#7E354D", 862 | "name": "Velvet Maroon" 863 | }, 864 | { 865 | "rgb": "#7D0552", 866 | "name": "Plum Velvet" 867 | }, 868 | { 869 | "rgb": "#7F4E52", 870 | "name": "Rosy Finch" 871 | }, 872 | { 873 | "rgb": "#7F5A58", 874 | "name": "Puce" 875 | }, 876 | { 877 | "rgb": "#7F525D", 878 | "name": "Dull Purple" 879 | }, 880 | { 881 | "rgb": "#B38481", 882 | "name": "Rosy Brown" 883 | }, 884 | { 885 | "rgb": "#C5908E", 886 | "name": "Khaki Rose" 887 | }, 888 | { 889 | "rgb": "#C48189", 890 | "name": "Pink Bow" 891 | } 892 | ] 893 | }, 894 | { 895 | "title": "Purples", 896 | "colors": [ 897 | { 898 | "rgb": "#7E587E", 899 | "name": "Viola Purple" 900 | }, 901 | { 902 | "rgb": "#571B7E", 903 | "name": "Purple Iris" 904 | }, 905 | { 906 | "rgb": "#583759", 907 | "name": "Plum Purple" 908 | }, 909 | { 910 | "rgb": "#4B0082", 911 | "name": "Indigo" 912 | }, 913 | { 914 | "rgb": "#461B7E", 915 | "name": "Purple Monster" 916 | }, 917 | { 918 | "rgb": "#4E387E", 919 | "name": "Purple Haze" 920 | }, 921 | { 922 | "rgb": "#614051", 923 | "name": "Eggplant" 924 | }, 925 | { 926 | "rgb": "#5E5A80", 927 | "name": "Grape" 928 | }, 929 | { 930 | "rgb": "#6A287E", 931 | "name": "Purple Jam" 932 | }, 933 | { 934 | "rgb": "#7D1B7E", 935 | "name": "Dark Orchid" 936 | }, 937 | { 938 | "rgb": "#8E35EF", 939 | "name": "Purple" 940 | }, 941 | { 942 | "rgb": "#893BFF", 943 | "name": "Aztech Purple" 944 | }, 945 | { 946 | "rgb": "#8467D7", 947 | "name": "Medium Purple" 948 | }, 949 | { 950 | "rgb": "#A23BEC", 951 | "name": "Jasmine Purple" 952 | }, 953 | { 954 | "rgb": "#B041FF", 955 | "name": "Purple Daffodil" 956 | }, 957 | { 958 | "rgb": "#C45AEC", 959 | "name": "Tyrian Purple" 960 | }, 961 | { 962 | "rgb": "#9172EC", 963 | "name": "Crocus Purple" 964 | }, 965 | { 966 | "rgb": "#9E7BFF", 967 | "name": "Purple Mimosa" 968 | }, 969 | { 970 | "rgb": "#D462FF", 971 | "name": "Heliotrope Purple" 972 | }, 973 | { 974 | "rgb": "#E238EC", 975 | "name": "Crimson" 976 | }, 977 | { 978 | "rgb": "#C38EC7", 979 | "name": "Purple Dragon" 980 | }, 981 | { 982 | "rgb": "#C8A2C8", 983 | "name": "Lilac" 984 | }, 985 | { 986 | "rgb": "#E6A9EC", 987 | "name": "Blush Pink" 988 | }, 989 | { 990 | "rgb": "#E0B0FF", 991 | "name": "Mauve" 992 | }, 993 | { 994 | "rgb": "#C6AEC7", 995 | "name": "Wisteria Purple" 996 | }, 997 | { 998 | "rgb": "#F9B7FF", 999 | "name": "Blossom Pink" 1000 | }, 1001 | { 1002 | "rgb": "#D2B9D3", 1003 | "name": "Thistle" 1004 | }, 1005 | { 1006 | "rgb": "#E9CFEC", 1007 | "name": "Periwinkle" 1008 | }, 1009 | { 1010 | "rgb": "#EBDDE2", 1011 | "name": "Lavender Pinocchio" 1012 | }, 1013 | { 1014 | "rgb": "#E3E4FA", 1015 | "name": "Lavender blue" 1016 | }, 1017 | { 1018 | "rgb": "#FDEEF4", 1019 | "name": "Pearl" 1020 | }, 1021 | { 1022 | "rgb": "#FFF5EE", 1023 | "name": "SeaShell" 1024 | } 1025 | ] 1026 | } 1027 | ] 1028 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby ">= 3.0.5" 4 | 5 | gem "cocoapods", "~> 1.13" 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.0.8) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | addressable (2.8.5) 12 | public_suffix (>= 2.0.2, < 6.0) 13 | algoliasearch (1.27.5) 14 | httpclient (~> 2.8, >= 2.8.3) 15 | json (>= 1.5.1) 16 | atomos (0.1.3) 17 | claide (1.1.0) 18 | cocoapods (1.13.0) 19 | addressable (~> 2.8) 20 | claide (>= 1.0.2, < 2.0) 21 | cocoapods-core (= 1.13.0) 22 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 23 | cocoapods-downloader (>= 1.6.0, < 2.0) 24 | cocoapods-plugins (>= 1.0.0, < 2.0) 25 | cocoapods-search (>= 1.0.0, < 2.0) 26 | cocoapods-trunk (>= 1.6.0, < 2.0) 27 | cocoapods-try (>= 1.1.0, < 2.0) 28 | colored2 (~> 3.1) 29 | escape (~> 0.0.4) 30 | fourflusher (>= 2.3.0, < 3.0) 31 | gh_inspector (~> 1.0) 32 | molinillo (~> 0.8.0) 33 | nap (~> 1.0) 34 | ruby-macho (>= 2.3.0, < 3.0) 35 | xcodeproj (>= 1.23.0, < 2.0) 36 | cocoapods-core (1.13.0) 37 | activesupport (>= 5.0, < 8) 38 | addressable (~> 2.8) 39 | algoliasearch (~> 1.0) 40 | concurrent-ruby (~> 1.1) 41 | fuzzy_match (~> 2.0.4) 42 | nap (~> 1.0) 43 | netrc (~> 0.11) 44 | public_suffix (~> 4.0) 45 | typhoeus (~> 1.0) 46 | cocoapods-deintegrate (1.0.5) 47 | cocoapods-downloader (1.6.3) 48 | cocoapods-plugins (1.0.0) 49 | nap 50 | cocoapods-search (1.0.1) 51 | cocoapods-trunk (1.6.0) 52 | nap (>= 0.8, < 2.0) 53 | netrc (~> 0.11) 54 | cocoapods-try (1.2.0) 55 | colored2 (3.1.2) 56 | concurrent-ruby (1.2.2) 57 | escape (0.0.4) 58 | ethon (0.16.0) 59 | ffi (>= 1.15.0) 60 | ffi (1.16.3) 61 | fourflusher (2.3.1) 62 | fuzzy_match (2.0.4) 63 | gh_inspector (1.1.3) 64 | httpclient (2.8.3) 65 | i18n (1.14.1) 66 | concurrent-ruby (~> 1.0) 67 | json (2.6.3) 68 | minitest (5.20.0) 69 | molinillo (0.8.0) 70 | nanaimo (0.3.0) 71 | nap (1.1.0) 72 | netrc (0.11.0) 73 | public_suffix (4.0.7) 74 | rexml (3.2.6) 75 | ruby-macho (2.5.1) 76 | typhoeus (1.4.0) 77 | ethon (>= 0.9.0) 78 | tzinfo (2.0.6) 79 | concurrent-ruby (~> 1.0) 80 | xcodeproj (1.23.0) 81 | CFPropertyList (>= 2.3.3, < 4.0) 82 | atomos (~> 0.1.3) 83 | claide (>= 1.0.2, < 2.0) 84 | colored2 (~> 3.1) 85 | nanaimo (~> 0.3.0) 86 | rexml (~> 3.2.4) 87 | 88 | PLATFORMS 89 | arm64-darwin 90 | x86_64-darwin 91 | 92 | DEPENDENCIES 93 | cocoapods (~> 1.13) 94 | 95 | RUBY VERSION 96 | ruby 3.2.1p31 97 | 98 | BUNDLED WITH 99 | 2.4.20 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Squarespace, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cwlcatchexception", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state" : { 8 | "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", 9 | "version" : "2.1.2" 10 | } 11 | }, 12 | { 13 | "identity" : "cwlpreconditiontesting", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state" : { 17 | "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", 18 | "version" : "2.1.2" 19 | } 20 | }, 21 | { 22 | "identity" : "nimble", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/Quick/Nimble", 25 | "state" : { 26 | "revision" : "edaedc1ec86f14ac6e2ca495b94f0ff7150d98d0", 27 | "version" : "12.3.0" 28 | } 29 | }, 30 | { 31 | "identity" : "quick", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/Quick/Quick", 34 | "state" : { 35 | "revision" : "ef9aaf3f634b3a1ab6f54f1173fe2400b36e7cb8", 36 | "version" : "7.3.0" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-simple-source", 7 | platforms: [ 8 | .iOS(.v13) 9 | ], 10 | products: [ 11 | .library( 12 | name: "SimpleSource", 13 | targets: ["SimpleSource"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/Quick/Quick", from: "7.3.0"), 18 | .package(url: "https://github.com/Quick/Nimble", from: "12.3.0"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "SimpleSource" 23 | ), 24 | .testTarget( 25 | name: "SimpleSourceTests", 26 | dependencies: [ 27 | "SimpleSource", 28 | .product(name: "Quick", package: "Quick"), 29 | .product(name: "Nimble", package: "Nimble"), 30 | ] 31 | ), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleSource 2 | 3 | [![ci](https://github.com/Squarespace/simple-source/actions/workflows/ci.yml/badge.svg)](https://github.com/Squarespace/simple-source/actions/workflows/ci.yml) 4 | [![codecov.io](https://codecov.io/github/Squarespace/simple-source/coverage.svg?branch=master)](https://codecov.io/github/Squarespace/simple-source?branch=master) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | [![CocoaPods compatible](https://img.shields.io/cocoapods/v/SimpleSource.svg)](https://cocoapods.org/pods/SimpleSource) 7 | 8 | ## Quick Start 9 | 10 | TL;DR? 11 | 12 | SimpleSource is a library that lets you populate and update table views and collection views 13 | with ease. It gives you fully typed closures so you don't have to cast views or items, 14 | it lets you deal with model objects instead of index paths, and it handles the cell bookkeeping 15 | for incremental updates. 16 | 17 | Run the example app. Navigate the UI and see how little code is in each view controller. 18 | Then come back here to learn more. 19 | 20 | ```sh 21 | cd Examples/ 22 | open SimpleSourceExample.xcworkspace 23 | ``` 24 | 25 | ## Contents 26 | 27 | - [Introduction](#introduction) 28 | - [Overview](#overview) 29 | - [Installation](#installation) 30 | - [Getting Started](#getting-started) 31 | - [Examples](#examples) 32 | - [Beyond the Basics](#beyond-the-basics) 33 | - [Getting Help](#getting-help) 34 | - [Contributing](#contributing) 35 | - [Acknowledgements](#acknowledgements) 36 | - [License](#license) 37 | 38 | ## Introduction 39 | 40 | Never implement `UITableViewDataSource` or `UICollectionViewDataSource` again. 41 | 42 | SimpleSource is a small, focused library that lets you 43 | 44 | - Populate and update `UITableView` and `UICollectionView` views from manually 45 | managed arrays or Core Data. 46 | - Forget about dequeuing and type casting cells. Forget about converting an 47 | `IndexPath` to a model object. SimpleSource will hand you dequeued views of the 48 | correct type along with the right model object for the index path. You can focus 49 | on applying your custom data to your custom view. 50 | - Forget about cell bookkeeping. Just mutate your data, and SimpleSource will update, 51 | add, and remove items and sections in your views. 52 | 53 | ### That's it? 54 | 55 | Those are the headline features, but sure, there's more. 56 | 57 | - **Automatic diffs and animated updates.** Store your items in a regular old Swift `Array`. 58 | Simply reassign or mutate the array, and the correct incremental changes will be 59 | automatically applied to your table view or collection view – animating the corresponding 60 | rows in and out. Same thing for Core Data. Say goodbye to `reloadData()`. 61 | - **Cleanly separate presentation logic from model data.** A SimpleSource *DataSource* 62 | object is all about the model data. It knows nothing about views. Not even whether it 63 | will be used to drive a table or collection view. Or how many views it will be delivering 64 | data to. 65 | - **Built-in or custom views?** You decide. For `UITableView` you can use any built-in 66 | `UITableViewCellStyle` for the cells. And for headers and footers you can use the built-in 67 | text-based ones, which only require you to provide a string to display. But of course you 68 | can also use custom views for cells, headers, and footers. 69 | - **Design in code or Interface Builder?** You decide. You tell SimpleSource how to dequeue 70 | your custom views. Either by directly instantiating a class, loading a NIB, using a 71 | storyboard prototype or (for table views) using one of the built-in cell styles. 72 | - **Easy reordering of collection view cells.** Do you want drag-and-drop for items in your 73 | collection view? By giving SimpleSource an optional reordering delegate you can have 74 | drag-and-drop reordering in as little as 1 line of code. Check out the example project. 75 | 76 | There will also be some slightly more advanced tips and tricks later in this document. Once 77 | we have covered basic usage. 78 | 79 | ## Overview 80 | 81 | There are 3 components involved when using SimpleSource. To populate a table view or 82 | collection view you will need exactly one of each. 83 | 84 | - **ViewDataSource** – This is the part you give to UIKit. It implements either 85 | `UITableViewDataSource` or `UICollectionViewDataSource` for you. To create one of these 86 | you need a *DataSource* and a *ViewFactory*. 87 | - **DataSource** – This is where your items come from. If you want to keep them organized 88 | in arrays use a `BasicDataSource`. If you have them in Core Data use a `CoreDataSource`. 89 | The *DataSource* knows absolutely nothing about views. 90 | - **ViewFactory** – This is responsible for creating and configuring your cells. Before 91 | creating the *ViewDataSource* you teach a *ViewFactory* how to create and configure all 92 | your different views from a model item. Later the *ViewDataSource* will find and pass 93 | the relevant model items to the *ViewFactory* for dequeueing and configuration, before 94 | finally sending the configured view to be displayed by UIKit. 95 | 96 | > **In summary:** 97 | > 98 | > The table view or collection view asks the *ViewDataSource* for a view to display for a given index 99 | > path. Using this index path, the *ViewDataSource* gets the corresponding model object from 100 | > the *DataSource* and gives it to the *ViewFactory*. The *ViewFactory* then dequeues a cell, 101 | > and uses the model object to configure the view before giving it back to the *ViewDataSource*. 102 | 103 | We will use the terms *ViewDataSource*, *ViewFactory* and *DataSource* to speak about these 104 | components in general. 105 | 106 | ![Chart](Web/chart.png) 107 | 108 | There are a few different concrete implementations of each component type, depending on where 109 | you get your data from (arrays or Core Data) and where you want to display it 110 | (a table or a collection view): 111 | 112 | | Component | Class Names | 113 | | ---------------- | -------------------------------------------------- | 114 | | *ViewDataSource* | `TableViewDataSource` / `CollectionViewDataSource` | 115 | | *ViewFactory* | `TableViewFactory` / `CollectionViewFactory` | 116 | | *DataSource* | `BasicDataSource` / `CoreDataSource` | 117 | 118 | ### What SimpleSource isn't 119 | 120 | SimpleSource is strictly a data source for your views. In particular, it doesn't want to be 121 | your view's delegate. Anything that has to do with cell/row selection, collection view layouts, 122 | row heights etc. is up to you and your own delegate code. 123 | 124 | The *DataSource* is not meant to be or replace your app's persistence layer: 125 | 126 | - A `CoreDataSource` is just a wrapper around an `NSFetchedResultsController` that you 127 | create from your own database. 128 | - A `BasicDataSource` is just a wrapper around a regular Swift array of items from anywhere 129 | you'd like. 130 | 131 | We have also kept the clever protocols and generics to a minimum. Table views and collection views 132 | have certain inherent differences. We accept that, and don't try to abstract everything away 133 | behind a single API, which matches neither. And you shouldn't have to be a type theorist to 134 | show an array of items in a table. 135 | 136 | ### What's the catch? 137 | 138 | There shouldn't be any catch. No one wants to give up control to an opaque library. 139 | 140 | With SimpleSource every moving part is either a closure which you provide, or an easily 141 | replaceable component. The library is quite small, and is mostly just a 142 | neat system for clicking different parts together into a flexible, functioning whole. 143 | 144 | As you read further down in this document you will see how to support custom databases, disable 145 | or adapt the animations to your liking etc. 146 | 147 | ## Installation 148 | 149 | ### CocoaPods 150 | 151 | To include SimpleSource in a project using [CocoaPods](http://cocoapods.org) add the following 152 | entry to your `Podfile`: 153 | 154 | ```ruby 155 | pod 'SimpleSource' 156 | ``` 157 | 158 | Then run the command `pod install` to add SimpleSource to your workspace. 159 | 160 | ### Swift Package Manager 161 | 162 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. 163 | 164 | You can add SimpleSource to an Xcode project by adding it as a package dependency. 165 | 166 | 1. From the **File** menu, select **Add Packages...** 167 | 1. Enter "https://github.com/Squarespace/simple-source" into the package repository URL text field 168 | 1. Depending on how your project is structured: 169 | - If you have a single application target that needs access to the library, then add 170 | **SimpleSource** directly to your application. 171 | - If you want to use this library from multiple Xcode targets, or mix Xcode targets and SPM 172 | targets, you must create a shared framework that depends on **SimpleSource** and 173 | then depend on that framework in all of your targets. 174 | 175 | If you developing a package, adding SimpleSource as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 176 | 177 | ```swift 178 | dependencies: [ 179 | .package(url: "https://github.com/Squarespace/simple-source", from: "3.0.1") 180 | ] 181 | ``` 182 | 183 | ## Getting Started 184 | 185 | We will build a simple example, showing a table of employees grouped by department. 186 | 187 | ### The Data 188 | 189 | Just like `UITableView` and `UICollectionView`, SimpleSource is built around the concept of 190 | items structured into sections. So our items will be employees, and our sections will be their 191 | department. 192 | 193 | We will use simple value types and arrays, so the `BasicDataSource` is right for the job. 194 | 195 | This will be our employee object: 196 | 197 | ```swift 198 | struct Employee { 199 | var name: String 200 | } 201 | ``` 202 | 203 | Now for the sections, which will be departments. A section here is anything conforming to the 204 | `SectionType` protocol. 205 | 206 | A section only has to provide an `items` array. But we are free to add more properties to a 207 | section, such as a title (or anything else we need) to properly configure section headers etc. 208 | 209 | To illustrate this, let's also add the department name to make the model a little richer. 210 | 211 | ```swift 212 | struct Department: SectionType { 213 | typealias ItemType = Employee 214 | var name: String 215 | var items: [ItemType] 216 | } 217 | ``` 218 | 219 | Now we can build our data set: 220 | 221 | ```swift 222 | // Employees 223 | let alice = Employee(name: "Alice") 224 | let bob = Employee(name: "Bob") 225 | ... 226 | 227 | // Departments 228 | let engineering = Department(name: "Engineering", items: [alice, christine, diana]) 229 | let sales = Department(name: "Sales", items: [bob, eliza, frank]) 230 | ... 231 | 232 | // Collect all departments 233 | let departments = [engineering, sales, ...] 234 | ``` 235 | 236 | ### The DataSource 237 | 238 | Once we have the data, creating a `BasicDataSource` is easy: 239 | 240 | ```swift 241 | let dataSource = BasicDataSource(sections: departments) 242 | ``` 243 | 244 | Note that `dataSource.sections` is a mutable array of `Department`. And for each section 245 | `section.items` is a mutable array of `Employee`. 246 | 247 | Once everything is up and running we can modify these arrays, and the table view will update 248 | automatically with the proper animations. 249 | 250 | ### The ViewFactory 251 | 252 | The next step on the way to a working table is to create a view factory. This will be 253 | responsible for creating and configuring the cells. 254 | 255 | A *ViewFactory* is created with a closure, which is called every time a new cell is about to be 256 | dequeued. It returns the reuse identifier for the cell. 257 | 258 | ```swift 259 | let viewFactory = TableViewFactory { item, view in 260 | return "Cell" 261 | } 262 | ``` 263 | 264 | > **Tip:** If you have more than one cell type in your view, look at the `item` passed to the 265 | > closure (in our case, `item` will be of type `Employee`). Then decide which kind of cell to 266 | > use and return the relevant reuse identifier. 267 | 268 | Now we must teach the view factory what cells to dequeue for the `"Cell"` reuse identifier 269 | and how to configure them. This is done through configuration closures. 270 | 271 | In this simple case we use vanilla `UITableViewCell`s, so that is what the closure gets. But if 272 | you have custom cell subclasses then that is what SimpleSource will send to your closure. No 273 | need for type casting. 274 | 275 | ```swift 276 | let configureCell = { (cell: UITableViewCell, employee: Employee, indexPath: IndexPath) -> Void in 277 | cell.textLabel?.text = employee.name 278 | } 279 | 280 | viewFactory.registerCell( 281 | method: .style(.default), 282 | reuseIdentifier: "Cell", 283 | in: tableView, 284 | configuration: configureCell 285 | ) 286 | ``` 287 | 288 | > **Tips:** 289 | > 290 | > If you are using a custom cell class it can be convenient to store the configuration closure 291 | > as a static class variable on the cell. Then pass (for example) `EmployeeCell.configureCell` 292 | > to `registerCell`. You can also store the reuse identifier this way. As, let's say, 293 | > `EmployeeCell.defaultReuseIdentifier`. 294 | > 295 | > If you use trailing closure syntax you can do the configuration as part of the 296 | > `registerCell` call. 297 | > 298 | > If your cell configuration closures require additional data not passed in by SimpleSource you can 299 | > capture those dependencies when you create the closures. You will see an example of this next 300 | > as we add the section header text to our table view. 301 | 302 | For good measure, let's also tell the `viewFactory` to add a text header for each department 303 | with the department name: 304 | 305 | ```swift 306 | viewFactory.registerHeaderText(in: tableView) { section in 307 | return dataSource.sections[section].name 308 | } 309 | ``` 310 | 311 | Notice how the configuration closure captures the data source here and uses it to get the name of 312 | the department for every section header. This is fine, since the data source does not hold a strong 313 | reference to anything but the model objects. But to avoid retain cycles you should be careful not 314 | to capture something which eventually retains the view factory. Use `[weak ...]` annotations on 315 | your configuration closures to break any retain cycles. 316 | 317 | ### The ViewDataSource 318 | 319 | Now we are ready to create the `UITableViewDataSource` for our table view. This is going to be 320 | an instance of `TableViewDataSource`. 321 | 322 | ```swift 323 | let tableViewDataSource = TableViewDataSource( 324 | dataSource: dataSource, 325 | viewFactory: viewFactory, 326 | viewUpdate: tableView.defaultViewUpdate() 327 | ) 328 | ``` 329 | 330 | This is where we connect the `dataSource` and the `viewFactory`. 331 | 332 | > **Note:** See the section on [live view updates](#live-view-updates) for an explanation 333 | > of the `viewUpdate` parameter. 334 | 335 | ### Connect the Data Source 336 | 337 | The only thing we need to do now is connect the `tableViewDataSource` to our table view: 338 | 339 | ```swift 340 | tableView.dataSource = tableViewDataSource 341 | ``` 342 | 343 | And our table is ready: 344 | 345 | ![Table](Web/employee-table.png) 346 | 347 | ### Live View Updates 348 | 349 | We haven't mentioned how changes made to a *DataSource* end up in the view. 350 | 351 | The *ViewDataSource* listens to the *DataSource* for data updates. These updates can either 352 | come from the `NSFetchedResultsController` given to a `CoreDataSource` or from a diff 353 | calculated by SimpleSource when you reassign the sections or item arrays in a `BasicDataSource` 354 | 355 | These changes then have to be applied to the view. 356 | 357 | When creating a *ViewDataSource* you also pass in a `viewUpdate` closure, which is responsible 358 | for incorporating incremental changes into the view. 359 | 360 | Most often you probably want to use one of the built-in row animations for table views, and 361 | use `performBatchUpdates` for collection views. 362 | 363 | For table views, SimpleSource defines `UITableView.defaultViewUpdate()` which does this 364 | animated update for you. If you prefer an unanimated update you can use 365 | `UITableView.unanimatedViewUpdate`. Or you can create your own. It's just a closure. You can 366 | also pass your favorite `UITableViewRowAnimation` to `defaultViewUpdate()` to customize it. 367 | 368 | For collection views, the built-in view updaters are called 369 | `UICollectionView.defaultViewUpdate` and `UICollectionView.unanimatedViewUpdate`. Any 370 | animations are provided by the collection view layout. See the UIKit documentation for 371 | `initialLayoutAttributesForAppearingItem(at:)` and friends. 372 | 373 | ## Examples 374 | 375 | There is a playground and an example project in the `Examples/` directory. 376 | 377 | To try it out, run the following commands: 378 | 379 | ```sh 380 | cd Examples/ 381 | open SimpleSourceExamples.xcworkspace 382 | ``` 383 | 384 | In this project you will see how to use both basic arrays and Core Data, how to create custom 385 | headers and footers, how the views update automatically when you mutate the data source, how to 386 | do drag-and-drop collection view reordering and more. 387 | 388 | > **Note:** If you want to try the playground, make sure you open it via the 389 | > `.xcworkspace` file. This will allow it to locate and build the necessary frameworks so 390 | > it can import SimpleSource. 391 | 392 | ## Beyond the Basics 393 | 394 | ### Collection View Reordering 395 | 396 | With SimpleSource, adding support for collection view cell reordering can be done in as 397 | little as one line of code. 398 | 399 | The first step is to make sure that the correct gesture handling is in place for your 400 | collection view. This is outside the scope for SimpleSource, but see the documentation for the 401 | property `installsStandardGestureForInteractiveMovement` on `UICollectionView`. Either set this 402 | property to `true` or install your own custom gestures. 403 | 404 | The `CollectionViewDataSource` class has an optional `reorderingDelegate` property which can be 405 | set to indicate that cell reordering should be enabled. 406 | 407 | This reordering delegate is defined by the `CollectionViewReorderingDelegate` protocol and is 408 | responsible for making the necessary modifications in the *DataSource* when reordering 409 | completes. 410 | 411 | Implementing it for a `BasicDataSource` only requires one line of actual code: 412 | 413 | ```swift 414 | func reordering(collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 415 | dataSource.moveItem(at: sourceIndexPath, to: destinationIndexPath) 416 | } 417 | ``` 418 | 419 | Implementing `CollectionViewReorderingDelegate` when using a `CoreDataSource` requires you to 420 | make modifications to your data that cause the object at `sourceIndexPath` to move to 421 | `destinationIndexPath`. How to do that depends on both the Core Data model and the sort 422 | criteria on the `NSFetchedResultsController` that the `CoreDataSource` was created from. 423 | 424 | See the example app for a demo of cell reordering. 425 | 426 | ### More Tips 427 | 428 | - **Do you like MVVM?** So do we! The view model is a great place to put your `DataSource`. 429 | Then keep the `ViewFactory` and `ViewDataSource` in the view layer. Either in your view 430 | controller or a helper class. 431 | - **Bring your own database.** Out of the box, SimpleSource delivers support for items stored 432 | in manually managed arrays as well as Core Data (using `NSFetchedResultsController`). If you 433 | are using a different database, it is easy to write your own data source by conforming to the 434 | `DataSourceType` protocol. The rest of SimpleSource – cell dequeuing, view updates, cell 435 | configuration etc. – is designed to be modular, and will work just fine with your custom data 436 | source. 437 | - **One data source. Multiple views.** Because a *DataSource* does not know anything about 438 | views, a single data source can be used to drive multiple views if necessary. 439 | - **Custom animations.** If you have particular needs for how data updates are incorporated 440 | into your views you can write your own `viewUpdate` closure and pass it to the 441 | *ViewDataSource*. 442 | - **Use multiple cell types.** If you have to display more than one cell type in your view, 443 | look at the `item` passed to the *ViewFactory* closure. Then decide which kind of cell to use 444 | and return the relevant reuse identifier. Each cell can then be configured using a type-safe 445 | closure, which gets an instance of that specific cell type and the item with which to 446 | configure it. 447 | - **Use multiple item types.** If you want to use multiple cell types, chances are good that 448 | the items in your data source should also have multiple types. They might not share a common 449 | base class or protocol. In this case a good solution can be to wrap them in a Swift `enum`. 450 | Make each item/cell type a `case` in your `enum`, and store the items as associated values on 451 | the `enum` entries. 452 | 453 | > **Example:** Imagine a typical settings screen in an app with support for different types of 454 | > preferences. We need many different cell and item types, so we define each type of preference 455 | > as a `case` in an `enum Preference`. Say the *ViewFactory* closure gets an item and sees that 456 | > it is `Preference.boolean(name: String, value: Bool)`. It knows to return 457 | > `SwitchCell.reuseIdentifier`. An instance of `SwitchCell` is now dequeued, and can be 458 | > configured using the associated `name` and `value` to set up the title label and on/off 459 | > switch for the preference. 460 | 461 | ## Getting Help 462 | 463 | If you believe you have found a bug in SimpleSource please open a GitHub issue. 464 | 465 | For general assistance please refer to the example app first. It covers almost the entire 466 | public API surface, and each screen is dedicated to a particular need or use case. 467 | 468 | If the example app does not answer your question ask it on 469 | [Stack Overflow](http://stackoverflow.com/questions/tagged/simplesource) 470 | and use the tag `simplesource`. 471 | 472 | ## Contributing 473 | 474 | Contributions in the form of pull requests are very welcome. 475 | 476 | Before your first pull request can be merged into the official SimpleSource repository, you 477 | will be asked to sign a contributor license agreement, allowing Squarespace Inc. to include 478 | your contribution. As the author you still reserve all right, title, and interest in and to 479 | your contributions. 480 | 481 | Please note that this project is released with a Contributor Code of Conduct. By participating 482 | in this project you agree to abide by its terms. See the CONTRIBUTING.md file for more 483 | information. 484 | 485 | ## Acknowledgements 486 | 487 | The authors would like to thank and acknowledge 488 | [TaylorSource](https://github.com/danthorpe/TaylorSource) by Dan Thorpe as an 489 | inspiration for SimpleSource. 490 | 491 | While the project scope, class hierarchy and protocol design of SimpleSource is quite different 492 | from TaylorSource, it informed a lot of the early design decisions. 493 | 494 | ## License 495 | 496 | [Apache 2.0](LICENSE) ([summary](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0))) 497 | 498 | Copyright (c) 2017, Squarespace, Inc. 499 | -------------------------------------------------------------------------------- /SimpleSource.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SimpleSource' 3 | s.version = '3.0.2' 4 | s.summary = 'Composable, easy to use data sources for UITableView and UICollectionView.' 5 | s.homepage = 'https://github.com/Squarespace/simple-source' 6 | s.license = { :type => 'Apache', :file => 'LICENSE' } 7 | s.authors = { 'Morten Heiberg' => 'mheiberg@squarespace.com', 'Thor Frolich' => 'tfrolich@squarespace.com' } 8 | s.platform = :ios, '13.0' 9 | s.swift_version = '5.7' 10 | s.source = { :git => 'https://github.com/Squarespace/simple-source.git', :tag => s.version } 11 | s.source_files = 'Sources/' + s.name + '/**/*.{h,m,swift}' 12 | 13 | s.test_spec 'Tests' do |test_spec| 14 | test_spec.resource = 'Tests/' + test_spec.name.gsub("/", "") + '/Resources/*.xcdatamodeld' 15 | test_spec.source_files = 'Tests/' + test_spec.name.gsub("/", "") + '/**/*.swift' 16 | test_spec.dependency 'Nimble', '~> 12.0' 17 | test_spec.dependency 'Quick', '~> 7.0' 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /Sources/SimpleSource/BasicData/BasicDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class BasicDataSource
: DataSourceType where Section: SectionType 4 | { 5 | public let updateHandler = IndexedUpdateHandler() 6 | 7 | public typealias Item = Section.Item 8 | 9 | public var sections: [Section] { 10 | didSet { 11 | let update = Diff.indexedUpdate(oldData: oldValue, newData: sections) 12 | updateHandler.send(update: update) 13 | } 14 | } 15 | 16 | public init(sections: [Section] = []) { 17 | self.sections = sections 18 | } 19 | 20 | public func numberOfSections() -> Int { 21 | return sections.count 22 | } 23 | 24 | public func numberOfItems(in sectionIndex: Int) -> Int { 25 | return sections[sectionIndex].items.count 26 | } 27 | 28 | public func sectionAtIndex(sectionIndex: Int) -> Section? { 29 | return sections[safe: sectionIndex] 30 | } 31 | 32 | public func item(at indexPath: IndexPath) -> Item? { 33 | return sections[safe: indexPath.section]?.items[safe: indexPath.item] 34 | } 35 | 36 | public func moveItem(at sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 37 | var sections = self.sections 38 | let item = sections[sourceIndexPath.section].items[sourceIndexPath.item] 39 | sections[sourceIndexPath.section].items.remove(at: sourceIndexPath.item) 40 | sections[destinationIndexPath.section].items.insert(item, at: destinationIndexPath.item) 41 | self.sections = sections 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SimpleSource/BasicData/BasicSection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A section usable with `BasicDataSource`. 4 | /// 5 | /// - Note: 6 | /// This protocol is only intended for use with a `BasicDataSource`. 7 | public protocol SectionType { 8 | associatedtype Item: Equatable 9 | var items: [Item] { get set } 10 | } 11 | 12 | /// Enables a `BasicDataSource` to perform in-place updates of modified sections instead of full reloads. 13 | /// 14 | /// `IdentifiableSection` is an optional protocol which sections of a `BasicDataSource` can conform to. 15 | /// 16 | /// It allows SimpleSource to recognize the sections between data updates. Instead of causing a full reload, 17 | /// changes to the section item arrays will then be analyzed, and a more detailed update will be emitted. 18 | /// 19 | /// - Note: 20 | /// This protocol is only intended for use with a `BasicDataSource`. 21 | public protocol IdentifiableSection { 22 | var sectionIdentifier: String { get } 23 | } 24 | 25 | /// Enables a `BasicDataSource` to perform in-place updates of modified items instead of full reloads. 26 | /// 27 | /// `IdentifiableItem` is an optional protocol which items of a `BasicDataSource` can conform to. 28 | /// 29 | /// It allows SimpleSource to recognize the items between data updates. Instead of changes to an item 30 | /// resulting in a deletion followed by an insertion, this protocol enables SimpleSource to coalesce 31 | /// these into an in-place update of the item instead. 32 | /// 33 | /// This can improve the appearance of table and collection view animations for changes. 34 | /// 35 | /// Make sure your sections conform to `IdentifiableSection`, or this will have no effect. 36 | /// 37 | /// - Note: 38 | /// This protocol is only intended for use with a `BasicDataSource`. 39 | public protocol IdentifiableItem { 40 | var itemIdentifier: String { get } 41 | } 42 | 43 | // MARK: - BasicSection 44 | 45 | /// A minimal section for use with `BasicDataSource`. 46 | /// 47 | /// This is only provided for convenience. Use a custom type instead for rich sections with attributes such as titles etc. 48 | public struct BasicSection: SectionType, Equatable { 49 | public typealias Item = ItemType 50 | public var items: [Item] 51 | 52 | public init(items: [Item]) { 53 | self.items = items 54 | } 55 | } 56 | 57 | extension BasicSection: ExpressibleByArrayLiteral { 58 | public init(arrayLiteral items: Item...) { 59 | self.items = items 60 | } 61 | } 62 | 63 | // MARK: - BasicIdentifiableSection 64 | 65 | /// A minimal identifiable section for use with `BasicDataSource`. 66 | /// 67 | /// This is only provided for convenience. Use a custom type instead for rich sections with attributes such as titles etc. 68 | public struct BasicIdentifiableSection: SectionType, IdentifiableSection, Equatable { 69 | public typealias Item = ItemType 70 | public var sectionIdentifier: String 71 | public var items: [Item] 72 | 73 | public init(sectionIdentifier: String, items: [Item]) { 74 | self.sectionIdentifier = sectionIdentifier 75 | self.items = items 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/SimpleSource/CoreData/CoreDataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | public class CoreDataSource: DataSourceType where Item: NSManagedObject 5 | { 6 | public let updateHandler = IndexedUpdateHandler() 7 | private lazy var fetchDelegate: FetchDelegate = FetchDelegate(updateHandler: self.updateHandler) 8 | 9 | public let fetchedResultsController: NSFetchedResultsController 10 | 11 | public init(fetchedResultsController: NSFetchedResultsController) { 12 | self.fetchedResultsController = fetchedResultsController 13 | self.fetchedResultsController.delegate = fetchDelegate 14 | do { 15 | try fetchedResultsController.performFetch() 16 | } catch let error { 17 | print("Error fetching: \(error)") 18 | } 19 | } 20 | 21 | public func numberOfFetchedItems() -> Int { 22 | guard let fetched = fetchedResultsController.fetchedObjects else { return 0 } 23 | return fetched.count 24 | } 25 | 26 | public func numberOfSections() -> Int { 27 | return fetchedResultsController.sections?.count ?? 0 28 | } 29 | 30 | public func numberOfItems(in sectionIndex: Int) -> Int { 31 | if let section = fetchedResultsController.sections?[sectionIndex] { 32 | return section.numberOfObjects 33 | } 34 | return 0 35 | } 36 | 37 | public func item(at indexPath: IndexPath) -> Item? { 38 | guard let fetchedObjects = fetchedResultsController.fetchedObjects, !fetchedObjects.isEmpty else { return nil } 39 | return fetchedResultsController.object(at: indexPath) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SimpleSource/CoreData/FetchDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CoreData 3 | 4 | class FetchDelegate: NSObject { 5 | 6 | private let updateHandler: IndexedUpdateHandler 7 | 8 | public init(updateHandler: IndexedUpdateHandler) { 9 | self.updateHandler = updateHandler 10 | } 11 | 12 | private struct PendingUpdates { 13 | var insertedSections = IndexSet() 14 | var updatedSections = IndexSet() 15 | var deletedSections = IndexSet() 16 | var insertedRows = Set() 17 | var updatedRows = Set() 18 | var deletedRows = Set() 19 | 20 | func createUpdate() -> IndexedUpdate { 21 | let update: IndexedUpdate = .delta( 22 | insertedSections: insertedSections, 23 | updatedSections: updatedSections, 24 | deletedSections: deletedSections, 25 | insertedRows: Array(insertedRows), 26 | updatedRows: Array(updatedRows), 27 | deletedRows: Array(deletedRows) 28 | ) 29 | return update 30 | } 31 | } 32 | 33 | private var pendingUpdates = PendingUpdates() 34 | 35 | } 36 | 37 | extension FetchDelegate: NSFetchedResultsControllerDelegate { 38 | public func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { 39 | switch (type) { 40 | case NSFetchedResultsChangeType.delete: 41 | pendingUpdates.deletedSections.insert(sectionIndex) 42 | case NSFetchedResultsChangeType.insert: 43 | pendingUpdates.insertedSections.insert(sectionIndex) 44 | default: 45 | break 46 | } 47 | } 48 | 49 | public func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { 50 | switch (type) { 51 | case NSFetchedResultsChangeType.insert: 52 | if indexPath == nil { // iOS 9 / Swift 2.0 BUG with running 8.4 (https://forums.developer.apple.com/thread/12184) 53 | if let newIndexPath = newIndexPath { 54 | pendingUpdates.insertedRows.insert(newIndexPath) 55 | } 56 | } 57 | case NSFetchedResultsChangeType.delete: 58 | if let indexPath = indexPath { 59 | pendingUpdates.deletedRows.insert(indexPath) 60 | } 61 | case NSFetchedResultsChangeType.update: 62 | if let indexPath = indexPath { 63 | pendingUpdates.updatedRows.insert(indexPath) 64 | } 65 | case NSFetchedResultsChangeType.move: 66 | if 67 | let newIndexPath = newIndexPath, 68 | let indexPath = indexPath 69 | { 70 | pendingUpdates.insertedRows.insert(newIndexPath) 71 | pendingUpdates.deletedRows.insert(indexPath) 72 | } 73 | @unknown default: 74 | break 75 | } 76 | } 77 | 78 | public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { 79 | let u = pendingUpdates.createUpdate() 80 | updateHandler.send(update: u) 81 | pendingUpdates = PendingUpdates() 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /Sources/SimpleSource/DataSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DataSourceType { 4 | associatedtype Item 5 | var updateHandler: IndexedUpdateHandler { get } 6 | func numberOfSections() -> Int 7 | func numberOfItems(in section: Int) -> Int 8 | func item(at indexPath: IndexPath) -> Item? 9 | } 10 | 11 | extension DataSourceType { 12 | public subscript(indexPath: IndexPath) -> Item? { 13 | return item(at: indexPath) 14 | } 15 | 16 | public func allItems() -> [Item] { 17 | return indexPathIterator().compactMap { item(at: $0) } 18 | } 19 | 20 | public func contains(indexPath: IndexPath) -> Bool { 21 | return indexPath.section < numberOfSections() && indexPath.item < numberOfItems(in: indexPath.section) 22 | } 23 | 24 | public func indexPathIterator() -> AnyIterator { 25 | let advanceIndexPath = { (indexPath: IndexPath?) -> IndexPath? in 26 | guard let indexPath = indexPath else { return nil } 27 | 28 | let nextItem = IndexPath(item: indexPath.item + 1, section: indexPath.section) 29 | if self.contains(indexPath: nextItem) { return nextItem } 30 | 31 | let nextSection = IndexPath(item: 0, section: indexPath.section + 1) 32 | if self.contains(indexPath: nextSection) { return nextSection } 33 | 34 | return nil 35 | } 36 | 37 | var nextIndexPath: IndexPath? = IndexPath(item: 0, section: 0) 38 | 39 | return AnyIterator { 40 | let result = nextIndexPath 41 | nextIndexPath = advanceIndexPath(nextIndexPath) 42 | return result 43 | } 44 | } 45 | } 46 | 47 | extension DataSourceType where Item: Equatable { 48 | public func indexPath(of item: Item) -> IndexPath? { 49 | return indexPathIterator().first { self.item(at: $0) == item } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UICollectionView/CollectionViewDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class CollectionViewDataSource: NSObject, UICollectionViewDataSource where DS: DataSourceType, VF: CollectionViewFactoryType, DS.Item == VF.Item { 4 | 5 | public typealias DataSource = DS 6 | public typealias ViewFactory = VF 7 | public typealias Item = DS.Item 8 | 9 | public let dataSource: DataSource 10 | public let viewFactory: ViewFactory 11 | 12 | // Used to temporarily ignore data source updates while reordering cells. 13 | private var ignoreDataSourceUpdates = false 14 | 15 | private var subscription: IndexedUpdateHandler.Subscription? 16 | 17 | public init(dataSource: DataSource, viewFactory: ViewFactory, viewUpdate: @escaping IndexedUpdateHandler.Observer) { 18 | self.dataSource = dataSource 19 | self.viewFactory = viewFactory 20 | 21 | super.init() 22 | 23 | self.subscription = dataSource.updateHandler.subscribe { [weak self] update in 24 | guard let `self` = self, !update.isEmpty, !self.ignoreDataSourceUpdates else { return } 25 | viewUpdate(update) 26 | } 27 | } 28 | 29 | public subscript(indexPath: IndexPath) -> Item? { 30 | return dataSource[indexPath] 31 | } 32 | 33 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 34 | return dataSource.numberOfSections() 35 | } 36 | 37 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 38 | return dataSource.numberOfItems(in: section) 39 | } 40 | 41 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 42 | guard 43 | let item = dataSource[indexPath], 44 | let cell = viewFactory.cell(for: item, in: collectionView, at: indexPath) 45 | else { 46 | fatalError("Configuration error.") 47 | } 48 | return cell 49 | } 50 | 51 | public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 52 | guard let supplementaryView = viewFactory.supplementaryView(ofKind: kind, in: collectionView, at: indexPath) else { 53 | fatalError("Configuration error.") 54 | } 55 | return supplementaryView 56 | } 57 | 58 | // MARK: Reordering 59 | 60 | public weak var reorderingDelegate: CollectionViewReorderingDelegate? 61 | 62 | public func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 63 | guard let reorderingDelegate = self.reorderingDelegate else { return false } 64 | return reorderingDelegate.reordering(collectionView: collectionView, canMoveItemAt: indexPath) 65 | } 66 | 67 | public func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 68 | guard let reorderingDelegate = self.reorderingDelegate else { return } 69 | ignoreDataSourceUpdates = true 70 | reorderingDelegate.reordering(collectionView: collectionView, moveItemAt: sourceIndexPath, to: destinationIndexPath) 71 | ignoreDataSourceUpdates = false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UICollectionView/CollectionViewFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum CollectionViewRegistrationMethod { 4 | case classBased(AnyClass?) 5 | case nibBased(UINib?) 6 | case dynamic 7 | } 8 | 9 | public protocol CollectionViewFactoryType { 10 | associatedtype Item 11 | 12 | func registerCell(method: CollectionViewRegistrationMethod, reuseIdentifier: String, in view: UICollectionView, configuration: @escaping (_ cell: C, _ item: Item, _ indexPath: IndexPath) -> Void) 13 | func cell(for item: Item, in view: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell? 14 | 15 | func registerSupplementaryView(method: CollectionViewRegistrationMethod, kind: String, reuseIdentifier: String, in view: UICollectionView, configuration: @escaping (_ supplementaryView: S, _ indexPath: IndexPath) -> Void) 16 | func supplementaryView(ofKind kind: String, in view: UICollectionView, at indexPath: IndexPath) -> UICollectionReusableView? 17 | } 18 | 19 | public class CollectionViewFactory: CollectionViewFactoryType { 20 | 21 | // These just make our internal types more readable. 22 | private typealias ContainerView = UICollectionView 23 | private typealias ReuseIdentifier = String 24 | private typealias SupplementaryViewKind = String 25 | private typealias ContainerViewIdentifier = Int 26 | private typealias CellProvider = (Item, ContainerView, IndexPath) -> Any? 27 | private typealias SupplementaryViewProvider = (ContainerView, IndexPath) -> Any? 28 | 29 | private let reuseIdentifierForItem: (Item, ContainerView) -> String 30 | private var cellProviders = [ContainerViewIdentifier : [ReuseIdentifier : CellProvider]]() 31 | private var supplementaryViewProviders = [ContainerViewIdentifier : [SupplementaryViewKind : SupplementaryViewProvider]]() 32 | 33 | public init(reuseIdentifierForItem: @escaping (Item, UICollectionView) -> String) { 34 | self.reuseIdentifierForItem = reuseIdentifierForItem 35 | } 36 | 37 | // MARK: - Cells 38 | 39 | public func registerCell(method: CollectionViewRegistrationMethod, reuseIdentifier: String, in view: UICollectionView, configuration: @escaping (_ cell: C, _ item: Item, _ indexPath: IndexPath) -> Void) { 40 | switch method { 41 | case .classBased(let c): 42 | view.register(c, forCellWithReuseIdentifier: reuseIdentifier) 43 | case .nibBased(let nib): 44 | view.register(nib, forCellWithReuseIdentifier: reuseIdentifier) 45 | case .dynamic: 46 | break 47 | } 48 | 49 | var cellProvidersForView = cellProviders[view.hashValue] ?? [:] 50 | cellProvidersForView[reuseIdentifier] = { (item, view, indexPath) -> C? in 51 | guard let cell = view.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? C else { return nil } 52 | configuration(cell, item, indexPath) 53 | return cell 54 | } 55 | cellProviders[view.hashValue] = cellProvidersForView 56 | } 57 | 58 | public func cell(for item: Item, in view: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell? { 59 | let reuseIdentifier = reuseIdentifierForItem(item, view) 60 | let cellProvider = cellProviders[view.hashValue]?[reuseIdentifier] 61 | return cellProvider?(item, view, indexPath) as? UICollectionViewCell 62 | } 63 | 64 | // MARK: - Supplementary Views 65 | 66 | public func registerSupplementaryView(method: CollectionViewRegistrationMethod, kind: String, reuseIdentifier: String, in view: UICollectionView, configuration: @escaping (_ supplementaryView: S, _ indexPath: IndexPath) -> Void) { 67 | switch method { 68 | case .classBased(let c): 69 | view.register(c, forSupplementaryViewOfKind: kind, withReuseIdentifier: reuseIdentifier) 70 | case .nibBased(let nib): 71 | view.register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: reuseIdentifier) 72 | case .dynamic: 73 | break 74 | } 75 | 76 | var supplementaryViewProvidersForView = supplementaryViewProviders[view.hashValue] ?? [:] 77 | supplementaryViewProvidersForView[kind] = { (view, indexPath) -> S? in 78 | guard let supplementaryView = view.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifier, for: indexPath) as? S else { return nil } 79 | configuration(supplementaryView, indexPath) 80 | return supplementaryView 81 | } 82 | supplementaryViewProviders[view.hashValue] = supplementaryViewProvidersForView 83 | } 84 | 85 | public func supplementaryView(ofKind kind: String, in view: UICollectionView, at indexPath: IndexPath) -> UICollectionReusableView? { 86 | let supplementaryViewProvider = supplementaryViewProviders[view.hashValue]?[kind] 87 | return supplementaryViewProvider?(view, indexPath) as? UICollectionReusableView 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UICollectionView/CollectionViewReorderingDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Conform to this protocol to enable reordering of collection view cells. 4 | /// 5 | /// The two methods correspond to the similarly named ones in `UICollectionViewDataSource`. 6 | /// 7 | /// Implementation note: We avoid using the exact same method names as in `UICollectionViewDataSource`. 8 | /// Doing so will confuse the Swift compiler when using a `UICollectionViewController` as the 9 | /// reordering delegate, since it already conforms to `UICollectionViewDataSource`, but the method 10 | /// implementations in Swift might not match the @objc requirements of that protocol. 11 | /// 12 | /// Set the `reorderingDelegate` property of your `CollectionViewDataSource` to enable reordering 13 | /// in the data source. 14 | public protocol CollectionViewReorderingDelegate: AnyObject { 15 | 16 | /// Return whether the item at the given index path can be moved or not. 17 | /// 18 | /// If you do not implement this method you will get a default implementation which returns `true` 19 | /// for every `IndexPath`. 20 | func reordering(collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool 21 | 22 | /// When a cell is dropped into a new position, this method is called. In here you must make the change 23 | /// to the `DataSource` which backs the `CollectionViewDataSource`. Actually moving the item to the new 24 | /// index path. 25 | /// 26 | /// - If you are using a `BasicDataSource` you can use the method `moveItem(at:to:)`. 27 | /// 28 | /// - If you are using a `CoreDataSource` you must make whatever changes to your database needed 29 | /// to make the item appear in the new place. This depends on the sort criteria of your 30 | /// `NSFetchedResultsController`. 31 | /// 32 | /// - If you are using a custom `DataSource` you probably know what to do. 33 | /// 34 | /// Until this method returns, the `CollectionViewDataSource` will ignore updates from the `DataSource` 35 | /// that supplies its data. This is to avoid moving the items twice. Once by dragging and again when you 36 | /// modify the `DataSource`. 37 | /// 38 | /// This is why you must complete the modifications before execution returns from this method. Do not dispatch 39 | /// asynchronously or otherwise perform the changes in the background. If you do, the changes will be picked up by the 40 | /// `CollectionViewDataSource` and the items will be moved again. Breaking the internal book-keeping of your 41 | /// collection view. 42 | func reordering(collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 43 | } 44 | 45 | extension CollectionViewReorderingDelegate { 46 | /// Default implementation. A reordering delegate can choose to omit this method in the common 47 | /// case where every cell can be moved. 48 | public func reordering(collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UICollectionView/UICollectionView+Updates.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UICollectionView { 4 | public var unanimatedViewUpdate: IndexedUpdateHandler.Observer { 5 | return { [weak self] _ in self?.reloadData() } 6 | } 7 | 8 | public var defaultViewUpdate: IndexedUpdateHandler.Observer { 9 | return { [weak self] update in 10 | guard let self = self else { return } 11 | 12 | if self.window == nil || update.isLikelyToCrashUIKitViews { 13 | self.reloadData() 14 | return 15 | } 16 | 17 | switch update { 18 | case let .delta(insertedSections, updatedSections, deletedSections, insertedRows, updatedRows, deletedRows): 19 | self.performBatchUpdates({ 20 | self.insertSections(insertedSections) 21 | self.deleteSections(deletedSections) 22 | self.reloadSections(updatedSections) 23 | self.insertItems(at: insertedRows) 24 | self.deleteItems(at: deletedRows) 25 | self.reloadItems(at: updatedRows) 26 | }, completion: { _ in }) 27 | case .full: 28 | self.reloadData() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UITableView/TableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class TableViewDataSource: NSObject, UITableViewDataSource where DS: DataSourceType, VF: TableViewFactoryType, DS.Item == VF.Item { 4 | 5 | public typealias DataSource = DS 6 | public typealias ViewFactory = VF 7 | public typealias Item = DS.Item 8 | 9 | public let dataSource: DataSource 10 | public let viewFactory: ViewFactory 11 | 12 | // Used to temporarily ignore data source updates while reordering rows. 13 | private var ignoreDataSourceUpdates = false 14 | 15 | private var subscription: IndexedUpdateHandler.Subscription? 16 | 17 | public init(dataSource: DataSource, viewFactory: ViewFactory, viewUpdate: @escaping IndexedUpdateHandler.Observer) { 18 | self.dataSource = dataSource 19 | self.viewFactory = viewFactory 20 | 21 | super.init() 22 | 23 | self.subscription = dataSource.updateHandler.subscribe { [weak self] update in 24 | guard let `self` = self, !update.isEmpty, !self.ignoreDataSourceUpdates else { return } 25 | viewUpdate(update) 26 | } 27 | } 28 | 29 | public subscript(indexPath: IndexPath) -> Item? { 30 | return dataSource[indexPath] 31 | } 32 | 33 | public func numberOfSections(in tableView: UITableView) -> Int { 34 | return dataSource.numberOfSections() 35 | } 36 | 37 | // MARK: - Cells 38 | 39 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 40 | return dataSource.numberOfItems(in: section) 41 | } 42 | 43 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 44 | guard 45 | let item = dataSource[indexPath], 46 | let cell = viewFactory.cell(for: item, in: tableView, at: indexPath) 47 | else { 48 | fatalError("Configuration error.") 49 | } 50 | return cell 51 | } 52 | 53 | // MARK: - Headers and Footers 54 | 55 | public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 56 | return viewFactory.headerText(forSection: section, in: tableView) 57 | } 58 | 59 | public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 60 | return viewFactory.footerText(forSection: section, in: tableView) 61 | } 62 | 63 | // NOTE: CUSTOM HEADER AND FOOTER VIEWS 64 | // 65 | // TableViewDataSource does not implement methods for custom header and footer views. 66 | // 67 | // That's because they are not the responsibility of a UITableViewDataSource. This is arguably 68 | // a design flaw in the UITableView API. 69 | // 70 | // If you use custom header and footer views you must implement these methods in your 71 | // UITableViewDelegate instead. 72 | // 73 | // SimpleSource can still assist you with dequeuing and configuring them! Just register them 74 | // with the TableViewFactory as normal. Then call the methods on the view factory from your 75 | // UITableViewDelegate to retrieve properly dequeued and configured views. 76 | // 77 | // The TableViewFactory methods for this are: 78 | // 79 | // func registerHeaderFooterView(method: _, reuseIdentifier: _, in: _, configuration: _) 80 | // func headerFooterView(reuseIdentifier: _, in: _, forSection: _) 81 | 82 | // MARK: - Editing 83 | 84 | public weak var editingDelegate: TableViewEditingDelegate? 85 | 86 | public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 87 | return editingDelegate?.editing(tableView: tableView, canEditRowAt: indexPath) ?? false 88 | } 89 | 90 | public func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 91 | editingDelegate?.editing(tableView: tableView, commit: editingStyle, forRowAt: indexPath) 92 | } 93 | 94 | // MARK: - Reordering 95 | 96 | public weak var reorderingDelegate: TableViewReorderingDelegate? 97 | 98 | public func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 99 | guard let reorderingDelegate = self.reorderingDelegate else { return false } 100 | return reorderingDelegate.reordering(tableView: tableView, canMoveRowAt: indexPath) 101 | } 102 | 103 | public func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 104 | guard let reorderingDelegate = self.reorderingDelegate else { return } 105 | ignoreDataSourceUpdates = true 106 | reorderingDelegate.reordering(tableView: tableView, moveRowAt: sourceIndexPath, to: destinationIndexPath) 107 | ignoreDataSourceUpdates = false 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UITableView/TableViewEditingDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Conform to this protocol to control editing of table view cells. 4 | /// 5 | /// The two methods correspond to the similarly named ones in `UITableViewDataSource`. 6 | /// 7 | /// Implementation note: We avoid using the exact same method names as in `UITableViewDataSource`. 8 | /// Doing so will confuse the Swift compiler when using a `UITableViewController` as the 9 | /// editing delegate, since it already conforms to `UITableViewDataSource`, but the method 10 | /// implementations in Swift might not match the @objc requirements of that protocol. 11 | /// 12 | /// Set the `editingDelegate` property of your `TableViewDataSource` to control editing. 13 | public protocol TableViewEditingDelegate: AnyObject { 14 | func editing(tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool 15 | func editing(tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) 16 | } 17 | 18 | extension TableViewEditingDelegate { 19 | /// Default implementation. An editing delegate can choose to omit this method in the common 20 | /// case where every item can be edited. 21 | public func editing(tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 22 | return true 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UITableView/TableViewFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum TableViewRegistrationMethod { 4 | case classBased(AnyClass?) 5 | case nibBased(UINib?) 6 | case style(UITableViewCell.CellStyle) 7 | case dynamic 8 | } 9 | 10 | // Headers and footers lack the support for UITableViewCellStyle. Hence this separate enum. 11 | public enum TableViewHeaderFooterRegistrationMethod { 12 | case classBased(AnyClass?) 13 | case nibBased(UINib?) 14 | case dynamic 15 | } 16 | 17 | public protocol TableViewFactoryType { 18 | associatedtype Item 19 | 20 | func registerCell(method: TableViewRegistrationMethod, reuseIdentifier: String, in view: UITableView, configuration: @escaping (_ cell: C, _ item: Item, _ indexPath: IndexPath) -> Void) 21 | func cell(for item: Item, in view: UITableView, at indexPath: IndexPath) -> UITableViewCell? 22 | 23 | func registerHeaderText(in view: UITableView, textProvider: @escaping (_ section: Int) -> String?) 24 | func headerText(forSection section: Int, in view: UITableView) -> String? 25 | func registerFooterText(in view: UITableView, textProvider: @escaping (_ section: Int) -> String?) 26 | func footerText(forSection section: Int, in view: UITableView) -> String? 27 | 28 | func registerHeaderFooterView(method: TableViewHeaderFooterRegistrationMethod, reuseIdentifier: String, in view: UITableView, configuration: @escaping (_ headerFooterView: V, _ section: Int) -> Void) 29 | func headerFooterView(reuseIdentifier: String, in view: UITableView, forSection section: Int) -> UIView? 30 | } 31 | 32 | public class TableViewFactory: TableViewFactoryType { 33 | 34 | // These just make our internal types more readable. 35 | private typealias ContainerView = UITableView 36 | private typealias ReuseIdentifier = String 37 | private typealias ContainerViewIdentifier = Int 38 | private typealias CellProvider = (Item, ContainerView, IndexPath) -> Any? 39 | private typealias HeaderFooterTextProvider = (Int) -> String? 40 | private typealias HeaderFooterViewProvider = (ContainerView, Int) -> Any? 41 | 42 | private let reuseIdentifierForItem: (Item, ContainerView) -> String 43 | private var cellProviders = [ContainerViewIdentifier : [ReuseIdentifier : CellProvider]]() 44 | private var headerTextProviders = [ContainerViewIdentifier : HeaderFooterTextProvider]() 45 | private var footerTextProviders = [ContainerViewIdentifier : HeaderFooterTextProvider]() 46 | private var headerFooterViewProviders = [ContainerViewIdentifier : [ReuseIdentifier : HeaderFooterViewProvider]]() 47 | 48 | public init(reuseIdentifierForItem: @escaping (Item, UITableView) -> String) { 49 | self.reuseIdentifierForItem = reuseIdentifierForItem 50 | } 51 | 52 | // MARK: - Cells 53 | 54 | public func registerCell(method: TableViewRegistrationMethod, reuseIdentifier: String, in view: UITableView, configuration: @escaping (_ cell: C, _ item: Item, _ indexPath: IndexPath) -> Void) { 55 | 56 | switch method { 57 | case .classBased(let c): 58 | view.register(c, forCellReuseIdentifier: reuseIdentifier) 59 | case .nibBased(let nib): 60 | view.register(nib, forCellReuseIdentifier: reuseIdentifier) 61 | case .style(_): 62 | if C.self != UITableViewCell.self { 63 | fatalError("Registering a UITableViewCellStyle is not supported for custom subclasses of UITableViewCell.") 64 | } 65 | case .dynamic: 66 | break 67 | } 68 | 69 | var cellProvidersForView = cellProviders[view.hashValue] ?? [:] 70 | cellProvidersForView[reuseIdentifier] = { (item, view, indexPath) -> C? in 71 | let optionalCell: C? 72 | if case .style(let style) = method { 73 | if let dequeued = view.dequeueReusableCell(withIdentifier: reuseIdentifier) { 74 | optionalCell = dequeued as? C 75 | } else { 76 | optionalCell = UITableViewCell(style: style, reuseIdentifier: reuseIdentifier) as? C 77 | } 78 | } else { 79 | optionalCell = view.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? C 80 | } 81 | guard let cell = optionalCell else { return nil } 82 | configuration(cell, item, indexPath) 83 | return cell 84 | } 85 | cellProviders[view.hashValue] = cellProvidersForView 86 | } 87 | 88 | public func cell(for item: Item, in view: UITableView, at indexPath: IndexPath) -> UITableViewCell? { 89 | let reuseIdentifier = reuseIdentifierForItem(item, view) 90 | let cellProvider = cellProviders[view.hashValue]?[reuseIdentifier] 91 | return cellProvider?(item, view, indexPath) as? UITableViewCell 92 | } 93 | 94 | // MARK: - Header/Footer Text 95 | 96 | public func registerHeaderText(in view: UITableView, textProvider: @escaping (_ section: Int) -> String?) { 97 | headerTextProviders[view.hashValue] = textProvider 98 | } 99 | 100 | public func headerText(forSection section: Int, in view: UITableView) -> String? { 101 | return headerTextProviders[view.hashValue]?(section) 102 | } 103 | 104 | public func registerFooterText(in view: UITableView, textProvider: @escaping (_ section: Int) -> String?) { 105 | footerTextProviders[view.hashValue] = textProvider 106 | } 107 | 108 | public func footerText(forSection section: Int, in view: UITableView) -> String? { 109 | return footerTextProviders[view.hashValue]?(section) 110 | } 111 | 112 | // MARK: - Custom Header/Footer Views 113 | 114 | // NOTE: 115 | // 116 | // These methods for custom header/footer views are not called from TableViewDataSource. The UITableView API 117 | // requires that these views are returned from the UITableViewDelegate. They are provided to make it more 118 | // convenient to implement this delegate functionality. The custom delegate can invoke these methods directly 119 | // on the view factory. 120 | 121 | public func registerHeaderFooterView(method: TableViewHeaderFooterRegistrationMethod, reuseIdentifier: String, in view: UITableView, configuration: @escaping (_ headerFooterView: V, _ section: Int) -> Void) { 122 | switch method { 123 | case .classBased(let c): 124 | view.register(c, forHeaderFooterViewReuseIdentifier: reuseIdentifier) 125 | case .nibBased(let nib): 126 | view.register(nib, forHeaderFooterViewReuseIdentifier: reuseIdentifier) 127 | case .dynamic: 128 | break 129 | } 130 | 131 | var headerFooterViewProvidersForView = headerFooterViewProviders[view.hashValue] ?? [:] 132 | headerFooterViewProvidersForView[reuseIdentifier] = { (view, section) -> V? in 133 | guard let headerFooterView = view.dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as? V else { return nil } 134 | configuration(headerFooterView, section) 135 | return headerFooterView 136 | } 137 | headerFooterViewProviders[view.hashValue] = headerFooterViewProvidersForView 138 | } 139 | 140 | public func headerFooterView(reuseIdentifier: String, in view: UITableView, forSection section: Int) -> UIView? { 141 | let headerFooterViewProvider = headerFooterViewProviders[view.hashValue]?[reuseIdentifier] 142 | return headerFooterViewProvider?(view, section) as? UIView 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UITableView/TableViewReorderingDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Conform to this protocol to enable reordering of table view cells. 4 | /// 5 | /// The two methods correspond to the similarly named ones in `UITableViewDataSource`. 6 | /// 7 | /// Implementation note: We avoid using the exact same method names as in `UITableViewDataSource`. 8 | /// Doing so will confuse the Swift compiler when using a `UITableViewController` as the 9 | /// reordering delegate, since it already conforms to `UITableViewDataSource`, but the method 10 | /// implementations in Swift might not match the @objc requirements of that protocol. 11 | /// 12 | /// Set the `reorderingDelegate` property of your `TableViewDataSource` to enable reordering 13 | /// in the data source. 14 | public protocol TableViewReorderingDelegate: AnyObject { 15 | 16 | /// Return whether the item at the given index path can be moved or not. 17 | /// 18 | /// If you do not implement this method you will get a default implementation which returns `true` 19 | /// for every `IndexPath`. 20 | func reordering(tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool 21 | 22 | /// When an item is dropped into a new position, this method is called. In here you must make the change 23 | /// to the `DataSource` which backs the `TableViewDataSource`. Actually moving the item to the new 24 | /// index path. 25 | /// 26 | /// - If you are using a `BasicDataSource` you can use the method `moveItem(at:to:)`. 27 | /// 28 | /// - If you are using a `CoreDataSource` you must make whatever changes to your database needed 29 | /// to make the item appear in the new place. This depends on the sort criteria of your 30 | /// `NSFetchedResultsController`. 31 | /// 32 | /// - If you are using a custom `DataSource` you probably know what to do. 33 | /// 34 | /// Until this method returns, the `TableViewDataSource` will ignore updates from the `DataSource` 35 | /// that supplies its data. This is to avoid moving the items twice. Once by dragging and again when you 36 | /// modify the `DataSource`. 37 | /// 38 | /// This is why you must complete the modifications before execution returns from this method. Do not dispatch 39 | /// asynchronously or otherwise perform the changes in the background. If you do, the changes will be picked up by the 40 | /// `TableViewDataSource` and the items will be moved again. Breaking the internal book-keeping of your 41 | /// table view. 42 | func reordering(tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 43 | } 44 | 45 | extension TableViewReorderingDelegate { 46 | /// Default implementation. A reordering delegate can choose to omit this method in the common 47 | /// case where every row can be moved. 48 | public func reordering(tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SimpleSource/UITableView/UITableView+Updates.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableView { 4 | public var unanimatedViewUpdate: IndexedUpdateHandler.Observer { 5 | return { [weak self] _ in self?.reloadData() } 6 | } 7 | 8 | public func defaultViewUpdate(with animation: UITableView.RowAnimation = .fade) -> IndexedUpdateHandler.Observer { 9 | return { [weak self] update in 10 | guard let self = self else { return } 11 | 12 | guard self.window != nil else { 13 | self.reloadData() 14 | return 15 | } 16 | 17 | switch update { 18 | case .delta(let insertedSections, let updatedSections, let deletedSections, let insertedRows, let updatedRows, let deletedRows): 19 | if update.isLikelyToCrashUIKitViews { break } 20 | self.beginUpdates() 21 | self.insertSections(insertedSections, with: animation) 22 | self.deleteSections(deletedSections, with: animation) 23 | self.reloadSections(updatedSections, with: animation) 24 | self.insertRows(at: insertedRows, with: animation) 25 | self.deleteRows(at: deletedRows, with: animation) 26 | self.reloadRows(at: updatedRows, with: animation) 27 | self.endUpdates() 28 | return 29 | case .full: 30 | break 31 | } 32 | 33 | // Fall-back animated full reload 34 | UIView.transition(with: self, duration: 0.4, options: .transitionCrossDissolve, animations: { 35 | self.reloadData() 36 | }, completion: nil) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SimpleSource/Updates.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum IndexedUpdate: Equatable { 4 | case delta( 5 | insertedSections: IndexSet, 6 | updatedSections: IndexSet, 7 | deletedSections: IndexSet, 8 | insertedRows: [IndexPath], 9 | updatedRows: [IndexPath], 10 | deletedRows: [IndexPath] 11 | ) 12 | case full 13 | 14 | /// Whether the update is considered empty. 15 | /// 16 | /// - `true` for delta updates with no content 17 | /// - `false` in all other cases 18 | public var isEmpty: Bool { 19 | switch self { 20 | case let .delta(insertedSections, updatedSections, deletedSections, insertedRows, updatedRows, deletedRows): 21 | return insertedSections.isEmpty && 22 | updatedSections.isEmpty && 23 | deletedSections.isEmpty && 24 | insertedRows.isEmpty && 25 | updatedRows.isEmpty && 26 | deletedRows.isEmpty 27 | case .full: 28 | return false 29 | } 30 | } 31 | 32 | var isLikelyToCrashUIKitViews: Bool { 33 | switch self { 34 | case let .delta(insertedSections, updatedSections, deletedSections, insertedRows, updatedRows, deletedRows): 35 | let hasItemUpdates = !(insertedRows.isEmpty && updatedRows.isEmpty && deletedRows.isEmpty) 36 | let hasSectionUpdates = !(insertedSections.isEmpty && updatedSections.isEmpty && deletedSections.isEmpty) 37 | return hasItemUpdates && hasSectionUpdates 38 | case .full: 39 | return false 40 | } 41 | } 42 | } 43 | 44 | public class IndexedUpdateHandler { 45 | 46 | public typealias Observer = ((IndexedUpdate) -> ()) 47 | 48 | // An opaque object, representing a subscription to updates. 49 | public class Subscription { 50 | private let onDeinit: () -> Void 51 | 52 | fileprivate init(onDeinit: @escaping () -> Void) { 53 | self.onDeinit = onDeinit 54 | } 55 | 56 | deinit { 57 | onDeinit() 58 | } 59 | } 60 | 61 | private typealias Token = String 62 | private var observers = [Token : Observer]() 63 | 64 | public init() {} 65 | 66 | deinit { 67 | observers.removeAll() 68 | } 69 | 70 | // Returns an opaque Subscription object, which must be retained for as long as the Observer 71 | // wants to receive updates. Release the Subscription object to deregister the Observer. 72 | public func subscribe(_ observer: @escaping Observer) -> Subscription { 73 | let token = NSUUID().uuidString 74 | observers[token] = observer 75 | return Subscription { [weak self] in 76 | self?.observers.removeValue(forKey: token) 77 | } 78 | } 79 | 80 | public func sendFullUpdate() { 81 | send(update: .full) 82 | } 83 | 84 | public func send(update: IndexedUpdate) { 85 | observers.values.forEach { $0(update) } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/SimpleSource/Utils/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array { 4 | subscript (safe index: Int) -> Iterator.Element? { 5 | return index < count ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/SimpleSource/Utils/Diff.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private struct WrappedIdentifiableSection { 4 | private let value: T 5 | private let section: IdentifiableSection 6 | 7 | init?(value: T) { 8 | guard let s = value as? IdentifiableSection else { return nil} 9 | self.value = value 10 | self.section = s 11 | } 12 | 13 | var sectionIdentifier: String { 14 | return section.sectionIdentifier 15 | } 16 | 17 | var items: [T.Item] { 18 | return value.items 19 | } 20 | } 21 | 22 | struct Diff { 23 | 24 | static func indexedUpdate(oldData: [T], newData: [T]) -> IndexedUpdate where T: SectionType { 25 | let wrappedOldData = oldData.compactMap { WrappedIdentifiableSection(value: $0) } 26 | let wrappedNewData = newData.compactMap { WrappedIdentifiableSection(value: $0) } 27 | guard (wrappedOldData.count == oldData.count) && (wrappedNewData.count == newData.count) else { return .full } 28 | return indexedUpdate(oldData: wrappedOldData, newData: wrappedNewData) 29 | } 30 | 31 | private static func indexedUpdate(oldData: [WrappedIdentifiableSection], newData: [WrappedIdentifiableSection]) -> IndexedUpdate { 32 | var insertedSections = IndexSet() 33 | var deletedSections = IndexSet() 34 | var insertedRows = [IndexPath]() 35 | var updatedRows = [IndexPath]() 36 | var deletedRows = [IndexPath]() 37 | 38 | let oldSectionIdentifiers = oldData.map { $0.sectionIdentifier } 39 | let newSectionIdentifiers = newData.map { $0.sectionIdentifier } 40 | 41 | newSectionIdentifiers.difference(from: oldSectionIdentifiers).forEach { change in 42 | switch change { 43 | case let .remove(offset, _, _): 44 | deletedSections.insert(offset) 45 | case let .insert(offset, _, _): 46 | insertedSections.insert(offset) 47 | } 48 | } 49 | 50 | newData.enumerated().forEach { sectionIndex, newSection in 51 | if insertedSections.contains(sectionIndex) { return } 52 | guard 53 | let oldSectionIndex = oldSectionIdentifiers.firstIndex(of: newSection.sectionIdentifier), 54 | let oldSection = oldData[safe: oldSectionIndex] else { return } 55 | 56 | // OPTIMIZATION: 57 | // 58 | // If `Item` conforms to `IdentifiableItem` and we can see that the section changes are strictly in-place updates 59 | // we update the items in place for this section, instead of deleting and reinserting them. 60 | // 61 | // KNOWN SHORTCOMING: 62 | // 63 | // This will not catch every in-place reload. If in-place updates are mixed with real inserts or deletes 64 | // they can be tricky to track. 65 | let oldIdentifiers = oldSection.items.compactMap { ($0 as? IdentifiableItem)?.itemIdentifier } 66 | let newIdentifiers = newSection.items.compactMap { ($0 as? IdentifiableItem)?.itemIdentifier } 67 | if oldIdentifiers.count == oldSection.items.count, newIdentifiers.count == newSection.items.count, oldIdentifiers == newIdentifiers { 68 | // All items are identifiable. The section contains the same items in the same order before and after the update. 69 | // We convert any changed items to in-place updates. 70 | updatedRows += zip(oldSection.items, newSection.items) 71 | .enumerated() 72 | .filter { $0.element.0 != $0.element.1 } 73 | .map { IndexPath(item: $0.offset, section: sectionIndex) } 74 | } else { 75 | // Calculate a diff to transform the old section items into the new section items. No in-place updates will be emitted. 76 | newSection.items.difference(from: oldSection.items).forEach { change in 77 | switch change { 78 | case let .remove(offset, _, _): 79 | deletedRows.append(.init(item: offset, section: oldSectionIndex)) 80 | case let .insert(offset, _, _): 81 | insertedRows.append(.init(item: offset, section: sectionIndex)) 82 | } 83 | } 84 | } 85 | } 86 | 87 | return .delta( 88 | insertedSections: insertedSections, 89 | updatedSections: IndexSet(), 90 | deletedSections: deletedSections, 91 | insertedRows: insertedRows, 92 | updatedRows: updatedRows, 93 | deletedRows: deletedRows 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/SimpleSourceTests/BasicDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | import Quick 4 | @testable import SimpleSource 5 | 6 | class BasicDataSourceTests: QuickSpec { 7 | 8 | override class func spec() { 9 | describe("A BasicDataSource") { 10 | 11 | typealias Section = BasicSection 12 | let sectionA = Section(items: ["A1", "A2", "A3"]) 13 | let sectionB = Section(items: ["B1", "B2"]) 14 | let sections = [sectionA, sectionB] 15 | let dataSource = BasicDataSource(sections: sections) 16 | 17 | it("has the expected number of sections") { 18 | expect(dataSource.numberOfSections()) == sections.count 19 | } 20 | 21 | it("has the expected number of items in each section") { 22 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 23 | expect(dataSource.numberOfItems(in: sectionIndex)) == sections[sectionIndex].items.count 24 | } 25 | } 26 | 27 | it("contains all the expected items via function lookup") { 28 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 29 | for itemIndex in 0 ..< dataSource.numberOfItems(in: sectionIndex) { 30 | let itemIndexPath = IndexPath(item: itemIndex, section: sectionIndex) 31 | expect(dataSource.item(at: itemIndexPath)) == sections[sectionIndex].items[itemIndex] 32 | } 33 | } 34 | } 35 | 36 | it("exposes the correct data via the sections variable") { 37 | expect(dataSource.sections) == sections 38 | } 39 | 40 | it("exposes the correct data via subscripting") { 41 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 42 | for itemIndex in 0 ..< dataSource.numberOfItems(in: sectionIndex) { 43 | let itemIndexPath = IndexPath(item: itemIndex, section: sectionIndex) 44 | expect(dataSource[itemIndexPath]) == dataSource.item(at: itemIndexPath) 45 | } 46 | } 47 | } 48 | 49 | it("exposes the correct data via subscripting") { 50 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 51 | for itemIndex in 0 ..< dataSource.numberOfItems(in: sectionIndex) { 52 | let itemIndexPath = IndexPath(item: itemIndex, section: sectionIndex) 53 | expect(dataSource[itemIndexPath]) == dataSource.item(at: itemIndexPath) 54 | } 55 | } 56 | } 57 | 58 | it("can move items within a section") { 59 | dataSource.moveItem( 60 | at: IndexPath(item: 0, section: 0), 61 | to: IndexPath(item: 2, section: 0)) 62 | 63 | expect(dataSource.sections) == [["A2", "A3", "A1"], sectionB] 64 | 65 | dataSource.moveItem( 66 | at: IndexPath(item: 2, section: 0), 67 | to: IndexPath(item: 0, section: 0)) 68 | 69 | expect(dataSource.sections) == sections 70 | } 71 | 72 | it("can move items between sections") { 73 | dataSource.moveItem( 74 | at: IndexPath(item: 0, section: 0), 75 | to: IndexPath(item: 0, section: 1)) 76 | 77 | expect(dataSource.sections) == [["A2", "A3"], ["A1", "B1", "B2"]] 78 | 79 | dataSource.moveItem( 80 | at: IndexPath(item: 2, section: 1), 81 | to: IndexPath(item: 1, section: 0)) 82 | 83 | expect(dataSource.sections) == [["A2", "B2", "A3"], ["A1", "B1"]] 84 | } 85 | 86 | it("exposes the correct content through allItems()") { 87 | expect(dataSource.allItems()) == ["A2", "B2", "A3", "A1", "B1"] 88 | 89 | dataSource.sections = sections 90 | 91 | expect(dataSource.allItems()) == sectionA.items + sectionB.items 92 | } 93 | 94 | it("can check if an index path is valid") { 95 | let validIndexPaths = [ 96 | IndexPath(item: 0, section: 0), 97 | IndexPath(item: 1, section: 0), 98 | IndexPath(item: 2, section: 0), 99 | IndexPath(item: 0, section: 1), 100 | IndexPath(item: 1, section: 1) 101 | ] 102 | 103 | for indexPath in validIndexPaths { 104 | expect(dataSource.contains(indexPath: indexPath)) == true 105 | } 106 | 107 | let invalidIndexPaths = [ 108 | IndexPath(item: 3, section: 0), 109 | IndexPath(item: 100, section: 0), 110 | IndexPath(item: 0, section: 10), 111 | IndexPath(item: 500, section: 5) 112 | ] 113 | 114 | for indexPath in invalidIndexPaths { 115 | expect(dataSource.contains(indexPath: indexPath)) == false 116 | } 117 | } 118 | 119 | it("can iterate across all valid index paths") { 120 | var collectedItems: [String] = [] 121 | for indexPath in dataSource.indexPathIterator() { 122 | if let item = dataSource[indexPath] { 123 | collectedItems.append(item) 124 | } 125 | expect(dataSource.contains(indexPath: indexPath)) == true 126 | } 127 | expect(collectedItems) == dataSource.allItems() 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Tests/SimpleSourceTests/CoreDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | @testable import SimpleSource 6 | 7 | private let StoreFilename = "SimpleSource-UnitTest-Store" 8 | 9 | class CoreDataSourceTests: QuickSpec { 10 | static let bundle: Bundle = { 11 | #if SWIFT_PACKAGE 12 | .module 13 | #else 14 | .init(for: CoreDataSourceTests.self) 15 | #endif 16 | }() 17 | 18 | static var model: NSManagedObjectModel = { 19 | let modelURL = bundle.url(forResource: "TestModel", withExtension: "momd")! 20 | return NSManagedObjectModel(contentsOf: modelURL)! 21 | }() 22 | 23 | static var persistentStoreCoordinator: NSPersistentStoreCoordinator = { 24 | let fileManager = FileManager.default 25 | let directoryURL = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 26 | let url = directoryURL.appendingPathComponent("\(StoreFilename).sqlite") 27 | try? fileManager.removeItem(at: url) 28 | let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) 29 | try? _ = coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) 30 | return coordinator 31 | }() 32 | 33 | static var context: NSManagedObjectContext = { 34 | let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 35 | managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator 36 | return managedObjectContext 37 | }() 38 | 39 | static var fetchController: NSFetchedResultsController = { () -> NSFetchedResultsController in 40 | let request: NSFetchRequest = TestEntity.fetchRequest() 41 | request.sortDescriptors = [NSSortDescriptor(key: #keyPath(TestEntity.name), ascending: true)] 42 | let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: #keyPath(TestEntity.section), cacheName: nil) 43 | try? controller.performFetch() 44 | return controller 45 | }() 46 | 47 | override class func spec() { 48 | describe("A CoreDataSource") { 49 | 50 | let dataSource = CoreDataSource(fetchedResultsController: fetchController) 51 | 52 | beforeEach { 53 | for object in self.fetchController.fetchedObjects ?? [] { 54 | self.context.delete(object) 55 | } 56 | try? self.context.save() 57 | } 58 | 59 | describe("being empty") { 60 | it("doesn't crash when querying an empty store") { 61 | let indexPath = IndexPath(item: 0, section: 0) 62 | expect(dataSource.item(at: indexPath)).to(beNil()) 63 | } 64 | } 65 | 66 | describe("having content") { 67 | var sections: [[TestEntity]]! 68 | 69 | beforeEach { 70 | let entityA = self.createEntity(name: "a", section: 0) 71 | let entityB = self.createEntity(name: "b", section: 1) 72 | sections = [[entityA], [entityB]] 73 | 74 | try? self.context.save() 75 | } 76 | 77 | it("has the expected number of sections") { 78 | expect(dataSource.numberOfSections()) == sections.count 79 | } 80 | 81 | it("has the expected number of items in each section") { 82 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 83 | expect(dataSource.numberOfItems(in: sectionIndex)) == sections[sectionIndex].count 84 | } 85 | } 86 | 87 | it("contains all the expected items via function lookup") { 88 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 89 | for itemIndex in 0 ..< dataSource.numberOfItems(in: sectionIndex) { 90 | let itemIndexPath = IndexPath(item: itemIndex, section: sectionIndex) 91 | expect(dataSource.item(at: itemIndexPath)) == sections[sectionIndex][itemIndex] 92 | } 93 | } 94 | } 95 | 96 | it("exposes the correct data via subscripting") { 97 | for sectionIndex in 0 ..< dataSource.numberOfSections() { 98 | for itemIndex in 0 ..< dataSource.numberOfItems(in: sectionIndex) { 99 | let itemIndexPath = IndexPath(item: itemIndex, section: sectionIndex) 100 | expect(dataSource[itemIndexPath]) == dataSource.item(at: itemIndexPath) 101 | } 102 | } 103 | } 104 | 105 | it("can check if an index path is valid") { 106 | let validIndexPaths = [ 107 | IndexPath(item: 0, section: 0), 108 | IndexPath(item: 0, section: 1) 109 | ] 110 | 111 | for indexPath in validIndexPaths { 112 | expect(dataSource.contains(indexPath: indexPath)) == true 113 | } 114 | 115 | let invalidIndexPaths = [ 116 | IndexPath(item: 3, section: 0), 117 | IndexPath(item: 100, section: 0), 118 | IndexPath(item: 0, section: 10), 119 | IndexPath(item: 500, section: 5) 120 | ] 121 | 122 | for indexPath in invalidIndexPaths { 123 | expect(dataSource.contains(indexPath: indexPath)) == false 124 | } 125 | } 126 | 127 | it("can iterate across all valid index paths") { 128 | var collectedItems: [TestEntity] = [] 129 | for indexPath in dataSource.indexPathIterator() { 130 | if let item = dataSource[indexPath] { 131 | collectedItems.append(item) 132 | } 133 | expect(dataSource.contains(indexPath: indexPath)) == true 134 | } 135 | expect(collectedItems) == dataSource.allItems() 136 | } 137 | } 138 | } 139 | } 140 | 141 | private static func createEntity(name: String, section: Int16) -> TestEntity { 142 | let entity = NSEntityDescription.insertNewObject(forEntityName: "TestEntity", into: context) as! TestEntity 143 | entity.name = name 144 | entity.section = section 145 | return entity 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Tests/SimpleSourceTests/IndexedUpdateHandlerTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | import Quick 4 | @testable import SimpleSource 5 | 6 | class IndexedUpdateHandlerTests: QuickSpec { 7 | 8 | override class func spec() { 9 | describe("An IndexedUpdateHandler") { 10 | let updateHandler = IndexedUpdateHandler() 11 | var subscriptions: [IndexedUpdateHandler.Subscription]! 12 | 13 | beforeEach { 14 | subscriptions = [] 15 | } 16 | 17 | it("forwards full updates to a subscribed observer") { 18 | var receivedFullUpdates = 0 19 | subscriptions.append(updateHandler.subscribe { update in 20 | if case .full = update { 21 | receivedFullUpdates = receivedFullUpdates + 1 22 | } else { 23 | fail("Unexpected update received.") 24 | } 25 | }) 26 | 27 | updateHandler.sendFullUpdate() 28 | expect(receivedFullUpdates) == 1 29 | 30 | updateHandler.send(update: .full) 31 | expect(receivedFullUpdates) == 2 32 | } 33 | 34 | it("forwards delta updates to a subscribed observer") { 35 | var receivedUpdates: [IndexedUpdate] = [] 36 | 37 | subscriptions.append(updateHandler.subscribe { update in 38 | receivedUpdates.append(update) 39 | }) 40 | 41 | let sentInsertedSections = IndexSet([1, 2]) 42 | let sentUpdatedSections = IndexSet([3, 4, 5]) 43 | let sentDeletedSections = IndexSet([6, 7]) 44 | let sentInsertedRows = [IndexPath(item: 1, section: 20)] 45 | let sentUpdatedRows = [IndexPath(item: 0, section: 1), IndexPath(item: 10, section: 11)] 46 | let sentDeletedRows = [IndexPath(item: 100, section: 100)] 47 | 48 | updateHandler.send(update: .delta( 49 | insertedSections: sentInsertedSections, 50 | updatedSections: sentUpdatedSections, 51 | deletedSections: sentDeletedSections, 52 | insertedRows: sentInsertedRows, 53 | updatedRows: sentUpdatedRows, 54 | deletedRows: sentDeletedRows)) 55 | 56 | expect(receivedUpdates.count) == 1 57 | 58 | switch receivedUpdates[0] { 59 | case let .delta( 60 | insertedSections: receivedInsertedSections, 61 | updatedSections: receivedUpdatedSections, 62 | deletedSections: receivedDeletedSections, 63 | insertedRows: receivedInsertedRows, 64 | updatedRows: receivedUpdatedRows, 65 | deletedRows: receivedDeletedRows): 66 | expect(receivedInsertedSections) == sentInsertedSections 67 | expect(receivedUpdatedSections) == sentUpdatedSections 68 | expect(receivedDeletedSections) == sentDeletedSections 69 | expect(receivedInsertedRows) == sentInsertedRows 70 | expect(receivedUpdatedRows) == sentUpdatedRows 71 | expect(receivedDeletedRows) == sentDeletedRows 72 | case .full: 73 | fail("Unexpected update received.") 74 | } 75 | } 76 | 77 | it("stops sending updates to removed observers") { 78 | var updatesForSubscriptionA = 0 79 | var updatesForSubscriptionB = 0 80 | 81 | subscriptions.append(updateHandler.subscribe { _ in 82 | updatesForSubscriptionA = updatesForSubscriptionA + 1 83 | }) 84 | 85 | updateHandler.sendFullUpdate() 86 | expect(updatesForSubscriptionA) == 1 87 | expect(updatesForSubscriptionB) == 0 88 | 89 | subscriptions.append(updateHandler.subscribe { _ in 90 | updatesForSubscriptionB = updatesForSubscriptionB + 1 91 | }) 92 | 93 | updateHandler.sendFullUpdate() 94 | expect(updatesForSubscriptionA) == 2 95 | expect(updatesForSubscriptionB) == 1 96 | 97 | _ = subscriptions.removeLast() 98 | 99 | updateHandler.sendFullUpdate() 100 | expect(updatesForSubscriptionA) == 3 101 | expect(updatesForSubscriptionB) == 1 102 | 103 | _ = subscriptions.removeLast() 104 | 105 | updateHandler.sendFullUpdate() 106 | expect(updatesForSubscriptionA) == 3 107 | expect(updatesForSubscriptionB) == 1 108 | 109 | expect(subscriptions.isEmpty) == true 110 | } 111 | 112 | it("properly removes observation closures") { 113 | class TestObject {} 114 | weak var weakObject: TestObject? 115 | do { 116 | let object = TestObject() 117 | weakObject = object 118 | subscriptions.append(updateHandler.subscribe { [object] _ in _ = object }) 119 | } 120 | 121 | expect(weakObject).toNot(beNil()) 122 | subscriptions = [] 123 | expect(weakObject).to(beNil()) 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Tests/SimpleSourceTests/Resources/TestModel.xcdatamodeld/TestModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Tests/SimpleSourceTests/UIKitViewUpdateTests.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import Quick 3 | import UIKit 4 | @testable import SimpleSource 5 | 6 | protocol DataSourceUpdateable: UIViewController { 7 | typealias Section = BasicIdentifiableSection 8 | var dataSource: BasicDataSource
{ get } 9 | var onViewUpdate: ((IndexedUpdate) ->Void)? { get set } 10 | var viewNumberOfSections: Int { get } 11 | init(sections: [Section]) 12 | func viewNumberOfRows(inSection: Int) -> Int 13 | } 14 | 15 | class UIKitViewUpdateTests: QuickSpec { 16 | typealias Section = BasicIdentifiableSection 17 | typealias DataSource = BasicDataSource
18 | 19 | static let cellIdentifier = "Cell" 20 | 21 | struct Item: Equatable, IdentifiableItem { 22 | let itemIdentifier: String 23 | var value: Bool 24 | } 25 | 26 | override class func spec() { 27 | let vcTypes: [DataSourceUpdateable.Type] = [TableViewController.self, CollectionViewController.self] 28 | for type in vcTypes { 29 | 30 | describe("A \(type) with a data source") { 31 | 32 | var initialSections: [Section]! 33 | var vc: DataSourceUpdateable! 34 | var window: UIWindow! 35 | 36 | beforeEach { 37 | initialSections = stride(from: 0, to: 3, by: 1).map { index -> Section in 38 | let item = Item(itemIdentifier: "\(index)", value: true) 39 | return Section(sectionIdentifier: "\(index)", items: [item]) 40 | } 41 | 42 | vc = type.init(sections: initialSections) 43 | window = UIWindow() 44 | window.rootViewController = vc 45 | vc.loadViewIfNeeded() 46 | window.makeKeyAndVisible() 47 | } 48 | 49 | 50 | it("updates its view") { 51 | expect(vc.viewNumberOfSections).toEventually(equal(initialSections.count)) 52 | expect(vc.viewNumberOfRows(inSection: 0)).toEventually(equal(initialSections[0].items.count)) 53 | } 54 | 55 | it("performs a simple diff'ed update") { 56 | var sections = initialSections! 57 | sections[0].items[0].value.toggle() 58 | sections[2].items[0].value.toggle() 59 | 60 | var didPerformDiffedUpdate = false 61 | var updateIsLikelyToCrashUITableView = true 62 | vc.onViewUpdate = { update in 63 | updateIsLikelyToCrashUITableView = update.isLikelyToCrashUIKitViews 64 | switch update { 65 | case .delta: 66 | didPerformDiffedUpdate = true 67 | case .full: 68 | break 69 | } 70 | } 71 | 72 | DispatchQueue.main.async { 73 | vc.dataSource.sections = sections 74 | } 75 | 76 | expect(updateIsLikelyToCrashUITableView).toEventually(equal(false)) 77 | expect(didPerformDiffedUpdate).toEventually(equal(true)) 78 | } 79 | 80 | it("survives a complex update") { 81 | // Move all items from section 1 to section 0 and delete section 1 82 | var sections = initialSections! 83 | sections[0].items.append(contentsOf: sections[1].items) 84 | sections.remove(at: 1) 85 | 86 | // Change the value of the first item in the (previous section 2 but now) section 1 87 | sections[1].items[0].value.toggle() 88 | 89 | var updateIsLikelyToCrashUITableView = false 90 | vc.onViewUpdate = { update in 91 | updateIsLikelyToCrashUITableView = update.isLikelyToCrashUIKitViews 92 | } 93 | 94 | DispatchQueue.main.async { 95 | vc.dataSource.sections = sections 96 | } 97 | 98 | expect(updateIsLikelyToCrashUITableView).toEventually(equal(true)) 99 | expect(vc.viewNumberOfSections).toEventually(equal(sections.count)) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | extension UIKitViewUpdateTests { 107 | fileprivate final class TableViewController: UITableViewController, DataSourceUpdateable { 108 | typealias ViewDataSource = TableViewDataSource> 109 | typealias Cell = UITableViewCell 110 | 111 | let sections: [Section] 112 | 113 | var onViewUpdate: ((IndexedUpdate) ->Void)? 114 | 115 | lazy var viewDataSource: ViewDataSource = { 116 | let dataSource = DataSource(sections: self.sections) 117 | let viewFactory = TableViewFactory { _,_ in UIKitViewUpdateTests.cellIdentifier } 118 | let configuration: (Cell, Item, IndexPath) -> Void = { (_, _, _) in } 119 | viewFactory.registerCell( 120 | method: .classBased(Cell.self), 121 | reuseIdentifier: UIKitViewUpdateTests.cellIdentifier, 122 | in: tableView, 123 | configuration: configuration 124 | ) 125 | let defaultViewUpdate = tableView.defaultViewUpdate() 126 | let viewUpdate: IndexedUpdateHandler.Observer = { [weak self] update in 127 | self?.onViewUpdate?(update) 128 | defaultViewUpdate(update) 129 | } 130 | return ViewDataSource( 131 | dataSource: dataSource, 132 | viewFactory: viewFactory, 133 | viewUpdate: viewUpdate 134 | ) 135 | }() 136 | 137 | var dataSource: BasicDataSource> { 138 | return viewDataSource.dataSource 139 | } 140 | 141 | var viewNumberOfSections: Int { return tableView!.numberOfSections } 142 | 143 | func viewNumberOfRows(inSection: Int) -> Int { 144 | return tableView.numberOfRows(inSection: inSection) 145 | } 146 | 147 | init(sections: [Section]) { 148 | self.sections = sections 149 | super.init(nibName: nil, bundle: nil) 150 | } 151 | 152 | required init?(coder: NSCoder) { 153 | fatalError("init(coder:) has not been implemented") 154 | } 155 | 156 | override func viewDidLoad() { 157 | super.viewDidLoad() 158 | tableView.dataSource = viewDataSource 159 | } 160 | } 161 | 162 | fileprivate final class CollectionViewController: UICollectionViewController, DataSourceUpdateable { 163 | typealias ViewDataSource = CollectionViewDataSource> 164 | typealias Cell = UICollectionViewCell 165 | 166 | let sections: [Section] 167 | 168 | var onViewUpdate: ((IndexedUpdate) ->Void)? 169 | 170 | lazy var viewDataSource: ViewDataSource = { 171 | let dataSource = DataSource(sections: self.sections) 172 | let viewFactory = CollectionViewFactory { _,_ in UIKitViewUpdateTests.cellIdentifier } 173 | let configuration: (Cell, Item, IndexPath) -> Void = { (_, _, _) in } 174 | viewFactory.registerCell( 175 | method: .classBased(Cell.self), 176 | reuseIdentifier: UIKitViewUpdateTests.cellIdentifier, 177 | in: collectionView, 178 | configuration: configuration 179 | ) 180 | let defaultViewUpdate = collectionView.defaultViewUpdate 181 | let viewUpdate: IndexedUpdateHandler.Observer = { [weak self] update in 182 | self?.onViewUpdate?(update) 183 | defaultViewUpdate(update) 184 | } 185 | return ViewDataSource( 186 | dataSource: dataSource, 187 | viewFactory: viewFactory, 188 | viewUpdate: viewUpdate 189 | ) 190 | }() 191 | 192 | var dataSource: BasicDataSource> { 193 | return viewDataSource.dataSource 194 | } 195 | 196 | var viewNumberOfSections: Int { return collectionView!.numberOfSections } 197 | 198 | func viewNumberOfRows(inSection: Int) -> Int { 199 | return collectionView.numberOfItems(inSection: inSection) 200 | } 201 | 202 | init(sections: [Section]) { 203 | self.sections = sections 204 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 205 | } 206 | 207 | required init?(coder: NSCoder) { 208 | fatalError("init(coder:) has not been implemented") 209 | } 210 | 211 | override func viewDidLoad() { 212 | super.viewDidLoad() 213 | collectionView.dataSource = viewDataSource 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Web/SimpleSource-figures.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Squarespace/simple-source/0254e1e6de815eafca3fb08655355127899510fc/Web/SimpleSource-figures.key -------------------------------------------------------------------------------- /Web/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Squarespace/simple-source/0254e1e6de815eafca3fb08655355127899510fc/Web/chart.png -------------------------------------------------------------------------------- /Web/employee-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Squarespace/simple-source/0254e1e6de815eafca3fb08655355127899510fc/Web/employee-table.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - Examples 4 | --------------------------------------------------------------------------------