Instagram / IGListKit

A data-driven UICollectionView framework for building fast and flexible lists.
https://instagram.github.io/IGListKit/
MIT License
12.88k stars 1.54k forks source link

[Swift] reference type and value type conformances to IGListDiffable #35

Closed rnystrom closed 7 years ago

rnystrom commented 8 years ago

The handy NSObject+IGListDiffable category doesn't really help with Swift objects like String and Int. This seems a little tricky because String is a struct but is convertible to an NSString. Doing that conversion gives us the NSObject<IGListDiffable> conformance, but that's kind of lame. I wonder if there's a way we can get this to work a little better?

For example, the Swift 3.0 compiler wont allow this:

let letters: [IGListDiffable] = ["a", "b", "c"]

But you can get this to work a few ways:

let strings: [NSString]  = ["a", "b", "c"]
let letters = strings as [IGListDiffable]
// or
let letters = ["a" as IGListDiffable, "b" as IGListDiffable, "c" as IGListDiffable]
// or
let letters: [IGListDiffable] = ["a" as NSString, "b" as NSString, "c" as NSString]

Also adding an extension to String isn't easy either, since String is a struct and IGListDiffable is an Objective-C protocol...

Note that this also stunts using IGListDiffable with Swift structs

jessesquires commented 8 years ago

Ah, interesting.

But, id<IGListDiffable> should now be imported as Any<IGListDiffable>, right? So Swift value types should be able to conform... ?

rnystrom commented 8 years ago

@jessesquires to my knowledge all ObjC protocols in Swift inherit from NSObjectProtocol even if we didn't declare it that way.

Just tried w/ this:

struct StructUser: IGListDiffable {
    let name: String
    let id: Int
}

And the compiler errors.

screen shot 2016-09-30 at 9 16 27 am

jessesquires commented 8 years ago

I see. I thought we declared this as @protocol IGListDiffable <NSObject>, but we don't. It's @protocol IGListDiffable.

Seems like this should be consider a Swift bug to me. We should search https://bugs.swift.org or ask around.

Just looked through the inter-op docs and didn't see anything about this. Intended behavior or not, for now I'm not sure if we can support swift value types.

Maybe we could provide a "Box" for swift value types:

public final class DiffableBox<T: Equatable>: IGListDiffable {

    let value: T
    let identifier: Any
    let equal: (T, T) -> Bool

    init(value: T, identifier: Any, equal: @escaping (T, T) -> Bool) {
        self.value = value
        self.identifier = identifier
        self.equal = equal
    }

    // IGListDiffable

    func diffIdentifier() -> Any {
        return identifier
    }

    func isEqual(obj: Any) -> Bool {
        if let other = obj as? T {
            return equal(value, other)
        }
        return false
    }
}

// Usage

let str = "my string"
let diffBox = DiffableBox(value: str, identifier: str, equal: ==)

Just a rough sketch, but you init with your value type, an identifier, and a equal closure/func.

You can package Swift + ObjC sources in a framework. If we add this, then this class should only be exposed to Swift clients and everything should just work.

Clients could .map their data to DiffableBoxs, or we might be able to provide a small Swift wrapper or something so that the framework could handle this.

jessesquires commented 8 years ago

Got an answer 😄

https://twitter.com/sanekgusev/status/781858018556129280

rnystrom commented 8 years ago

Bummer, makes sense tho. I wish there was something like NS_REFINED_FOR_SWIFT for this.

Boxing is an awesome idea though. Probably good to provide something like that.

rnystrom commented 8 years ago

@jessesquires check out _ObjectiveCBridgeable (blog post here). I wonder if we can use something there?

h/t to @nlutsenko https://twitter.com/nlutsenko/status/782652105530019840

jessesquires commented 8 years ago

Interesting.

  1. That post is a year old and a lot has changed. I remember some proposals/mailing list discussions about _ObjectiveCBridgeable, but I can't remember the details — it might have changed.
  2. Also, using _Protocol (underscore) protocols from the std lib is considered bad practice. These are "private".
jessesquires commented 8 years ago

Ah yes, this was removed in Swift 3:

Swift 2.2 docs: http://swiftdoc.org/v2.2/protocol/_ObjectiveCBridgeable/ Not found for 3.0: http://swiftdoc.org/v3.0/

screen shot 2016-10-03 at 7 44 59 am

jessesquires commented 8 years ago

Ah, perhaps not removed, but now properly private.

https://github.com/apple/swift-evolution/blob/master/proposals/0058-objectivecbridgeable.md

https://lists.swift.org/pipermail/swift-evolution-announce/2016-April/000095.html

rnystrom commented 8 years ago

Ok good to know. I was looking at it more over the weekend too and don't think it would've helped. I think boxing is going to be the way to go for now.

jessesquires commented 8 years ago

Ah, ReferenceConvertible?

https://twitter.com/benasher44/status/783034260370235392

rnystrom commented 8 years ago

Messed around w/ it, not sure how we can add an IGListDiffable extension though. There has to be a way for this to work...

matthewcheok commented 8 years ago

Is there still no way we can have a swift struct conform to IGListDiffable?

rnystrom commented 8 years ago

@matthewcheok nope 😕 when using Swift objects have to be a class. I'm sure there's a good pattern by boxing with something like Box<StructType> where Box is a class conforming to IGListDiffable.

jessesquires commented 7 years ago

Going to close this since there's nothing actionable right now (and for the foreseeable future)

danielgalasko commented 7 years ago

Inspired by @jessesquires I will put my solution in here for enabling swift value types to conform to Diffable. I ended up with a new Swift protocol Diffable and used Equatable as the means for exposing isEqual(toDiffableObject object) from IGListKit.

I then wrap diff'ing around a struct called DiffUtility

/**
 A diffable value type that can be used in conjunction with
 `DiffUtility` to perform a diff between two result sets.
 */
public protocol Diffable: Equatable {

    /**
     Returns a key that uniquely identifies the object.

     - returns: A key that can be used to uniquely identify the object.

     - note: Two objects may share the same identifier, but are not equal.

     - warning: This value should never be mutated.
     */
    var diffIdentifier: String { get }
}

/**
 Performs a diff operation between two sets of `ItemDiffable` results.
 */
public struct DiffUtility {

    public struct DiffResult {
        public typealias Move = (from: Int, to: Int)
        public let inserts: [Int]
        public let deletions: [Int]
        public let updates: [Int]
        public let moves: [Move]

        public let oldIndexForID: (_ id: String) -> Int
        public let newIndexForID: (_ id: String) -> Int
    }

    public static func diff<T: Diffable>(originalItems: [T], newItems: [T]) -> DiffResult {
        let old = originalItems.map({ DiffableBox(value: $0, identifier: $0.diffIdentifier as NSObjectProtocol, equal: ==) })
        let new = newItems.map({ DiffableBox(value: $0, identifier: $0.diffIdentifier as NSObjectProtocol, equal: ==) })
        let result = IGListDiff(old, new, .equality)

        let inserts = Array(result.inserts)
        let deletions = Array(result.deletes)
        let updates = Array(result.updates)

        let moves: [DiffResult.Move] = result.moves.map({ (from: $0.from, to: $0.to) })

        let oldIndexForID: (_ id: String) -> Int = { id in
            return result.oldIndex(forIdentifier: NSString(string: id))
        }
        let newIndexForID: (_ id: String) -> Int = { id in
            return result.newIndex(forIdentifier: NSString(string: id))
        }
        return DiffResult(inserts: inserts, deletions: deletions, updates: updates, moves: moves, oldIndexForID: oldIndexForID, newIndexForID: newIndexForID)
    }
}

private final class DiffableBox<T: Diffable>: IGListDiffable {

    let value: T
    let identifier: NSObjectProtocol
    let equal: (T, T) -> Bool

    init(value: T, identifier: NSObjectProtocol, equal: @escaping(T, T) -> Bool) {
        self.value = value
        self.identifier = identifier
        self.equal = equal
    }

    // IGListDiffable

    func diffIdentifier() -> NSObjectProtocol {
        return identifier
    }

    func isEqual(toDiffableObject object: IGListDiffable?) -> Bool {
        if let other = object as? DiffableBox<T> {
            return equal(value, other.value)
        }
        return false
    }
}
rnystrom commented 7 years ago

@danielgalasko 😲 this is super cool! cc @jessesquires

Przemyslaw-Wosko commented 7 years ago

@danielgalasko reading this took me some time, before i understood it. Could you put here small example of usage?

danielgalasko commented 7 years ago

sure thing @CurlyHeir but its very similar to how you would use IGListKit normally. So lets start with a struct:

struct TestDiff: Diffable {
    var name: String
    let id: String

    var diffIdentifier: String {
        return id
    }

    static func ==(lhs: TestDiff, rhs: TestDiff) -> Bool {
        return lhs.name == rhs.name
    }
}
let bob = TestDiff(name: "Bob", id: "1")
let tigger = TestDiff(name: "tigger", id: "2")
let initial = [bob]
let new = [bob, tigger]
let diff = DiffUtility.diff(originalItems: initial, newItems: new)
//diff.inserts == 1

Not sure if thats what you were asking but thats how I use it

Eke commented 7 years ago

Hello @danielgalasko ! How do you use Diffable objects in public func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] ?

danielgalasko commented 7 years ago

@Eke you will see I created DiffableBox that takes a Diffable object and transforms it into IGListDiffable.

diffableItems.map({ DiffableBox(value: $0, identifier: $0.diffIdentifier as NSObjectProtocol, equal: ==) })

Kinda like that :)

vibrazy commented 7 years ago

Just created an extension to Sequence to wrap DiffableBox. Still got hope for a solution one day :)

extension String: Diffable {
    public var diffIdentifier: String { return self }
}

extension Int: Diffable {
    public var diffIdentifier: String { return String(self) }
}

extension Sequence where Iterator.Element: Diffable {
    func diffable() -> [ListDiffable] {
        let toListDiffable: [ListDiffable] = map{ DiffableBox(value: $0, identifier: $0.diffIdentifier as NSObjectProtocol, equal: ==) }
        return toListDiffable
    }
}

// usage
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    return ["Daniel", "Hollis", "Tavares"].diffable()
}
tunidev commented 7 years ago

is this still the way to go ? any full example please ? @danielgalasko

levi commented 7 years ago

Chiming in here. Would be fantastic to simply use Hashable conformance on the Swift side in place of ListDiffable, since it provides both identity and equality comparison. Much easier said than done, however, since ListDiffable a core type of the diffing algorithm. Not much flexibility off the top of my head without either constructing a DiffableBox like @jessesquires above or maintaining a completely separate Swift adapter implementation. I'm going to do some more independent thinking/research and see what could be realized.

@rnystrom Instagram looking to adopt Swift anytime soon? 😂

tunidev commented 6 years ago

@levi that would be awesome, any progress ? the DiffableBox works but it adds unnecessary code to the project. I hope that we can use Hashable soon 🥇

rnystrom commented 6 years ago

Just letting you all know that I have a Swift bridge layer idea in progress that will handle all of this under the hood, letting you use Swift structs with IGListKit!

Sent with GitHawk

levi commented 6 years ago

Thanks for the update, Ryan!

JUSTINMKAUFMAN commented 6 years ago

@rnystrom First off, this is such a killer library you've built!

I'm just wondering if there's been any movement on the Swift bridge layer mentioned above for enabling the use of Swift structs? Thanks!

Arcovv commented 6 years ago

@vibrazy HI @danielgalasko I really love your solution, but met some problems.

Seems func diffable() -> [ListDiffable] tries to map the value using DiffableBox.

When I want to use them in a ListAdapterDataSource, in func objects(for listAdapter: ListAdapter) -> [ListDiffable] using your extension is really awesome.

But when I try to use in func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController, the object actually the type of DiffableBox<T>, the typecast process will crash my code.

And DiffableBox<T> is private so can't access the real value.

Should you think make DiffableBox<T> as public or something to solve this problem?

Thanks for your help!

matthewweldon commented 6 years ago

@Arcovv I think I know the problem you are having. When you're making the switch for your sectionControllerFor object: you have to actually define the concrete type you're expecting the Diffable box to conform to.

example of concrete diffablebox vs a non concrete switch object { case is DiffableBox<YourStruct>: //your type needs to look like this return CFLCardSectionController() case is DiffableBox<Any>: //your type cannot be this return AccessCardSectionController()// casting will likely fail with this implementation }

edit: after rereading your comment, I suspect I have a newer version of the diffablebox, so maybe what I'm saying doesn't apply

staticdreams commented 6 years ago

Any progress officially supporting this? @rnystrom

claudiogomezGL commented 5 years ago

@rnystrom Any progress supporting this?

jonathansolorzn commented 5 years ago

2019 Already, is this still a WIP?

iwasrobbed-ks commented 5 years ago

I would just look into using DifferenceKit or the new iOS 13 api's for this instead

wonder2011 commented 2 years ago

Is 2022. Nothing yet ?