GithubHelp home page GithubHelp logo

shopify / functionaltabledata Goto Github PK

View Code? Open in Web Editor NEW
365.0 402.0 30.0 2.02 MB

Declarative UITableViewDataSource implementation

License: MIT License

Swift 99.78% Objective-C 0.22%
functional-programming user-interface ios swift declarative uitableview uicollectionview

functionaltabledata's Issues

Add CI badge

We are missing the travis badge in the README file

Feature request: TableCell highlighting

UITableViewCells have a function built in: setHighlighted(_ highlighted: Bool, animated: Bool)`, which allows you to manipulate any subviews as needed when the user touches down on a cell.

With FunctionalTableData, this callback does not occur for free. There's currently no way in a HostCell implementation to override this callback.

The most common use case is when you have a UIView or UIImageView with an opaque background color. On highlight, UITableViewCell will automatically remove the background color for any subviews to ensure the selection color is visible. In the cases where you want to retain the background color of any subviews during selection and highlighting with a UITableViewCell you'd simply override setHighlighted and ensure the background is set for either state.

CellActions currently provides some state overriding, such as SelectionState, however none exist for a highlighted state.

I reckon something like the following would be good:

public typealias HighlightingAction = (_ sender: UIView, highlighted: Bool) -> HighlightingState

with

public enum HighlightingState {
	case highlighted
	case unhighlighted
}

At the same time, I do see two callbacks for selection/deselection, so highlighting could have the same two, or perhaps selection could be reconfigured to one callback to have the two be the same:

public typealias SelectionAction = (_ sender: UIView, selected: Bool) -> SelectionState

Default separator values

What

At the moment you need to specify this somewhere in your app to make the separators appear. We should consider providing these as default

Separator.appearance().backgroundColor = UITableView().separatorColor
Separator.inset = 16

Convert our keys to being AnyHashable instead of String

CellConfigType, TableSection, and ItemPath all use String's for their key's, we should instead use AnyHashable which makes things a bit nicer and easier to use (no need to convert Hashable key types to a String before they can be used as a key)

Crash in FTD

I found this crash in our POS app store build. Couldn't get more info than this but this at least has filenames and line numbers that might help locate the problem.

Thread 11 Crashed:
0   libswiftCore.dylib                   0x0000000102b0e410 0x102884000 + 2663440
1   FunctionalTableData                  0x0000000101c836ec function signature specialization <Arg[4] = Dead> of FunctionalTableData.TableSectionChangeSet.(isRow in _9D04CC2439151915054CA13314D61D66)(new: (section: FunctionalTableData.TableSection, row: Swift.Int), equalTo: (section: FunctionalTableData.TableSection, row: Swift.Int)) -> Swift.Bool (TableSectionChangeSet.swift:164)
2   FunctionalTableData                  0x0000000101c7ea84 FunctionalTableData.TableSectionChangeSet.(compareRows in _9D04CC2439151915054CA13314D61D66)(newRows: inout Swift.Set<Swift.String>, oldRows: inout [Swift.String : Swift.Int], oldSectionIndex: Swift.Int, newSectionIndex: Swift.Int) -> () (TableSectionChangeSet.swift:0)
3   FunctionalTableData                  0x0000000101c7ce5c FunctionalTableData.TableSectionChangeSet.(calculateChanges in _9D04CC2439151915054CA13314D61D66)() -> () (TableSectionChangeSet.swift:137)
4   FunctionalTableData                  0x0000000101c8406c function signature specialization <Arg[0] = Owned To Guaranteed, Arg[1] = Owned To Guaranteed, Arg[2] = Owned To Guaranteed> of FunctionalTableData.TableSectionChangeSet.init(old: [FunctionalTableData.TableSection], new: [FunctionalTableData.TableSection], visibleIndexPaths: [Foundation.IndexPath]) -> FunctionalTableData.TableSectionChangeSet (TableSectionChangeSet.swift:76)
5   FunctionalTableData                  0x0000000101c7249c function signature specialization <Arg[0] = Owned To Guaranteed, Arg[3] = Owned To Guaranteed> of FunctionalTableData.FunctionalTableData.(doRenderAndDiff in _1CE3C8B4455DD2DC43A2D2D02278E711)(_: [FunctionalTableData.TableSection], animated: Swift.Bool, animations: FunctionalTableData.FunctionalTableData.TableAnimations, completion: () -> ()?) -> () (FunctionalTableData.swift:0)
6   FunctionalTableData                  0x0000000101c6b3d8 closure #1 () -> () in FunctionalTableData.FunctionalTableData.renderAndDiff(_: [FunctionalTableData.TableSection], animated: Swift.Bool, animations: FunctionalTableData.FunctionalTableData.TableAnimations, completion: () -> ()?) -> () (FunctionalTableData.swift:0)
7   FunctionalTableData                  0x0000000101c786e8 partial apply forwarder for closure #1 () -> () in FunctionalTableData.FunctionalTableData.renderAndDiff(_: [FunctionalTableData.TableSection], animated: Swift.Bool, animations: FunctionalTableData.FunctionalTableData.TableAnimations, completion: () -> ()?) -> () (FunctionalTableData.swift:0)
8   FunctionalTableData                  0x0000000101c525a4 reabstraction thunk helper from @escaping @callee_guaranteed () -> () to @escaping @callee_unowned @convention(block) () -> () (FunctionalCollectionData.swift:0)
9   Foundation                           0x00000001bf0dab6c __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 12
10  Foundation                           0x00000001befe2cc8 -[NSBlockOperation main] + 68
11  Foundation                           0x00000001befe219c -[__NSOperationInternal _start:] + 736
12  Foundation                           0x00000001bf0dca40 __NSOQSchedule_f + 268
13  libdispatch.dylib                    0x00000001be0866c8 _dispatch_call_block_and_release + 20
14  libdispatch.dylib                    0x00000001be087484 _dispatch_client_callout + 12
15  libdispatch.dylib                    0x00000001be02a874 _dispatch_continuation_pop$VARIANT$mp + 408
16  libdispatch.dylib                    0x00000001be029f3c _dispatch_async_redirect_invoke + 596
17  libdispatch.dylib                    0x00000001be036a60 _dispatch_root_queue_drain + 372
18  libdispatch.dylib                    0x00000001be037308 _dispatch_worker_thread2 + 124
19  libsystem_pthread.dylib              0x00000001be269190 _pthread_wqthread + 468
20  libsystem_pthread.dylib              0x00000001be26bd00 start_wqthread + 0

Adding support for table row deletion

Hi, I would like to understand how to correctly handle table view row deletions with FunctionalTableData. I understand that we should not work with indexes, however right now this is what I came up with:

class MyViewController: UITableViewController {
    internal let functionalData = FunctionalTableData()
    
    internal var items: [Translation] = [] {
        didSet {
            render()
        }
    }
    
    private func render() {
        let cellStyle = CellStyle(bottomSeparator: .inset, separatorColor: .gray, backgroundColor: .black)
        let rows: [CellConfigType] = items.enumerated().map { index, item in
            return HistoryCell(
                key: "id-\(index)",
                style: cellStyle,
                actions: CellActions(rowActions: [UITableViewRowAction(style: .destructive, title: "Delete", handler: ({ (rowAction, indexPath) in
                    self.items.remove(at: indexPath.row)
                }))]),
                state: HistoryState(translationItem: item),
                cellUpdater: HistoryState.updateView)
        }
        
        functionalData.renderAndDiff([
            TableSection(key: "section", rows: rows)
            ])
    }

    ...
}

My intuition was that I needed to create a new CellAction, however, I'm not entirely sure how to handle the data deletion from the array (it's not working well, more specifically when I try to delete any row the last row gets deleted). I would appreciate any help with this, thanks!

Reuses the deleted row which makes the new row in editing mode

Issue:
When there are multiple rows in a section and when I try to delete a row, it reuses the deleted row for the immediate row which shouldn't. This makes the row below the deleted row to be in editing mode.

if !changes.deletedRows.isEmpty {
tableView.deleteRows(at: changes.deletedRows, with: animations.rows.delete)
}

Adding tableView.setEditing(false) when deleting the rows in the snippet fixes the issue. But that wouldn't be an appropriate solution. So we might need to look into a better solution.

functionaltable

Deselecting table view cell automatically after selection

How would I go about deselecting a table view cell? I'm currently using

let cellStyle = CellStyle(bottomSeparator: .inset,
                          separatorColor: .gray,
                          highlight: true,
                          selectionColor: UIColor(named: "selectedCell"),
                          backgroundColor: .black)

This code highlights the cell on tap but then the cell never gets deselected afterwards. I took a look at the CellStyle docs but couldn't find a propriety that handles deselection. I'm aware there are CellActions I can use (specifically the deselectionAction), although the action returns an item/index so not sure if I can use that.

How would I go about having functional table data deselect my cell automatically? Thanks :)

screenshot

Usage with Xib

Hello!

Thank you for contributing this to the community! I am trying it out and am having a hard time understanding how to create HostCell cells who's view comes from a nib. I don't see any way that tableview's register(_ nib: UINib?, forCellReuseIdentifier identifier: String) method can work and I've tried to have the views instantiated by HostCell load nibs in their initializers and ran into some issues there too.

Do you have a best practice for doing this with your framework?

Thanks!
Dan

Editing style for a row isn't well defined

We are basing the editing style on the presence of rowActions/leading/trailing config. This isn't right but is needed for the moment for backwards compatibility.
7950466

We should instead provide a canDelete or deleteAction type mechanism that this delegate method would utilize. Should also look into that 3rd insert state that I can't remember the purpose of anymore

Reduce CI notifications

Travis is kind of noisy, update the yml with

notifications:
  email:
    on_success: never
    on_failure: always

New Difference engine

Remove our custom difference computation and replace with a more maintainable one.

Current options on the table are DifferenceKit and/or Swift 5.1's built in Collection Difference. The Swift 5.1 one however would require a "polyfill" type approach to support OS's that do not have that built in

Issues with using FunctionalTableData

Hey there, I'm trying to get my hands on this library but I'm having issues with using it. I installed FunctionalTableData through Carthage and then copied the sample classes (table view controller and label cell) to the newly project I've created. However, after running the project and tapping the plus button to add a new table view entry a crash occurs.

Specifically, it looks like there are some issues in the TableSectionChangeSet file. I couldn't specifically track down what is going on, but here is the sample project I am using (https://github.com/cesaredecal/ShopifyFunctionalTableDataIssue). This issue is occurring on every single Xcode project I test this library on. Maybe there's something wrong with my Carthage configuration or something else in general. Happy to provide more info if necessary.

I've also tried to install this library the other way, by simply dragging and dropping the FunctionalTableData folder into the project. However this leads to conflicts on my end (duplicate info.plist files and so on...). So I'm not really sure what to do at this point.

Thanks for your help!

Deprecate (and remove?) CellStyle.backgroundView

The CellStyle.backgroundView property is too expensive. Views should only be created when they're needed. This background view property means that the view is created and destroyed on every render pass even though it's rarely used.

Sunset playgrounds

Loading the framework inside the playground works very sporadically. You have to delete and write again the same code over and over until the compiler finally picks it up

Include the closure-based addTarget code

It's tough to use the vanilla addTarget(selector:) when you've got a table that includes a list of UI elements that are backed by some list of objects, and want to know which item in the list was interacted with. The closure-based addTarget makes it easy because you can capture the data item currently being interacted with (and the view controller itself to trigger a re-render).

So in my naive and humble opinion the closure-based addTarget should come with FTD.

Also, hi :)

Revisit new CellAction previewingViewControllerAction process

The initializer for CellAction now takes a PreviewingViewControllerAction closure for the previewingViewControllerAction argument. Implementors of this closure are expected to call previewingContext.sourceRect = previewingContext.sourceView.convert(cell.bounds, from: cell).

Since I imagine the majority of use cases for PreviewingViewControllerAction will behave the same way, it would be nice if we could have the nominal path for this do the sourceRect assignment automatically.

FunctionalTableData v2.0 Ideas

I've been thinking about some potential changes to this library that could mean breaking small API changes. I don't have working implementations of these things so it's a list of dreams right now.

  1. Cell and Row IDs should be AnyHashable instead of String
    • In our apps we're converted typed IDs to Strings and back to IDs when we need to use them when there's no real reason that the ID couldn't be used directly.
  2. Codable TableSection and CellConfig
    • This would allow us to persist our current state easily and write a snapshot style test assert that a section matches the recorded encoded section.
  3. Use Difference Kit instead of our custom diffing algorithm.
    • We occasionally have to debug TableSectionChangeSet to find the cause of an issue. The code's difficult to understand and we aren't getting the benefit of the number of eyes that DifferenceKit has.

Row deletions - extra argument in call: rowActions

Hi, I previously used to delete rows by specifying a UITableViewRowAction in rowActions property like so

let deleteRowAction = UITableViewRowAction(style: .destructive, title: "Delete", handler: ({ (rowAction, indexPath) in
    self.didTapDeleteOnRow(indexPath: indexPath)
}))

let rows: [CellConfigType] = getSourceArray().enumerated().map { index, item in
    return HistoryCell(
        key: "index-\(index)-\(item.translatedText)",
        style: cellStyle,
        actions: CellActions(
            selectionAction: { _ in
                self.didSelectCell(translation: item)
                return .selected
        },
            deselectionAction: { _ in
                return .deselected
        }, rowActions: [deleteRowAction]),   // <---- here
        state: HistoryState(translationItem: item),
        cellUpdater: HistoryState.updateView)
}

Now I see rowActions isn't part of the initializer anymore, so I'm wondering how to enable cell deletion. Any insights?

Touches cancelled when long pressed

What

When long pressing on a UICollectionViewCell using FunctionalCollectionData, the touch will be cancelled. Unlike its UITableViewCell counterpart.

To reproduce, create a FCD cell that has a selectionColor and selectionAction. Long press on it and then release, the selection closure won't fire and the color will revert back before releasing

Incorrect contentOffset being set on pull-to-refresh

There is an issue with the UITableView setting an incorrect contentOffset during FunctionalTableData.applyTableChanges(...).applyTableSectionChanges(...). This results in the following "white flash" / choppy animation when pulling to refresh.

  • It looks like this issue was only introduced in iOS 12.1 (it cannot be reproduced on any device on any earlier version of iOS)
  • Reproduced on iPad and iPhone (notched & un-notched).

Frames dropped when a large number of sections are reused

TableView jumps when it is scrolled up ๐Ÿ›

How to Replicate ๐Ÿ›

  1. Run this demo project, tap Table Sections Demo to view TableSectionsViewController, where there is a list of ProgramicDetailCells, each within its own TableSection.
  2. Scroll down the table.
  3. Tap a cell to re-render the table.
  4. Scroll up. The table will jump when a new TableSection is displayed.

issue

Solution

The tableView jumps because estimatedHeightForHeaderInSection and estimatedHeightForFooterInSection have not been implemented in FunctionalTableData.
This implementation fixes the issue.

public func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
    return heightForHeaderInSection(tableViewStyle: tableView.style, section: section)
}

public func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
    return heightForFooterInSection(tableViewStyle: tableView.style, section: section)
}

public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return heightForHeaderInSection(tableViewStyle: tableView.style, section: section)
}

public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
    return heightForFooterInSection(tableViewStyle: tableView.style, section: section)
}

private func heightForHeaderInSection(tableViewStyle: UITableViewStyle, section: Int) -> CGFloat {
    guard let header = sections[section].header else {
        // When given a height of zero grouped style UITableView's use their default value instead of zero. By returning CGFloat.min we get around this behavior and force UITableView to end up using a height of zero after all.
        return tableViewStyle == .grouped ? minimumHeaderHeight : 0
    }
    return header.height
}

private func heightForFooterInSection(tableViewStyle: UITableViewStyle, section: Int) -> CGFloat {
    guard let footer = sections[section].footer else {
        // When given a height of zero grouped style UITableView's use their default value instead of zero. By returning CGFloat.min we get around this behavior and force UITableView to end up using a height of zero after all.
        return tableViewStyle == .grouped ? minimumHeaderHeight : 0
    }
    return footer.height
}

private var minimumHeaderHeight: CGFloat {
    if #available(iOS 11.0, *) {
        return CGFloat.leastNormalMagnitude
    } else {
        return 2.0
    }
}

UIContextualAction won't render both image and text if cell is <= 90pts

http://www.openradar.me/33107135

In FunctionalTableData, when the contextual actions are being created, we can determine if the cell is this height and instead return a pre-rendered (and likely cached) image that includes both the text and the image. This might be a configurable property to allow it to be opted in or out.

This could have some side effects in scenarios where the cell changes size while the action is visible (device rotation, tableview gets wider).

API niggles and edge cases

Here are a few rough edges in the API we could clean up at some point:

  • Setting FunctionalTableData.tableView should unsubscribe the FTD instance as the delegate and dataSource of the old tableView (if any).
  • keyPathForIndexPath should return nil for index paths that are out of range, instead of dying with an out-of-bounds error, to make the public API friendlier and to be consistent with indexPathFromKeyPath. It could also be renamed to keyPath(for: IndexPath) to be more Swift 4y.
  • Likewise, indexPathFromKeyPath should be renamed to indexPathForKeyPath or indexPath(for: KeyPath).
  • rectForKeyPath returns nil if the KeyPath couldn't be found, but may also return CGRect.zero if the corresponding IndexPath couldn't be found. This shouldn't happen in practice, but we should still catch and map that value to nil also, to handle the no-value case consistently.
  • sectionForKey could be trivially reimplemented with sections.first { $0.key == key }

Recommend Projects

  • React photo React

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

  • Vue.js photo Vue.js

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

  • Typescript photo Typescript

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

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

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

Recommend Topics

  • javascript

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

  • web

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

  • server

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

  • Machine learning

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

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

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

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

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

  • D3 photo D3

    Data-Driven Documents codes.