Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
rnystrom opened this issue Sep 29, 2016 · 35 comments
Closed

Comments

@rnystrom
Copy link
Contributor

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
Copy link
Contributor

Ah, interesting.

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

@rnystrom
Copy link
Contributor Author

@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
Copy link
Contributor

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
Copy link
Contributor

Got an answer 😄

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

@rnystrom
Copy link
Contributor Author

rnystrom commented Sep 30, 2016

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
Copy link
Contributor Author

rnystrom commented Oct 2, 2016

@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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor Author

rnystrom commented Oct 3, 2016

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
Copy link
Contributor

Ah, ReferenceConvertible?

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

@rnystrom
Copy link
Contributor Author

rnystrom commented Oct 3, 2016

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

@jessesquires jessesquires changed the title Swift standard object conformance to IGListDiffable [Swift] reference and value type conformances to IGListDiffable Oct 12, 2016
@jessesquires jessesquires changed the title [Swift] reference and value type conformances to IGListDiffable [Swift] reference type and value type conformances to IGListDiffable Oct 12, 2016
@matthewcheok
Copy link

matthewcheok commented Nov 10, 2016

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

@rnystrom
Copy link
Contributor Author

@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
Copy link
Contributor

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

@danielgalasko
Copy link

danielgalasko commented Feb 5, 2017

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
Copy link
Contributor Author

rnystrom commented Feb 5, 2017

@danielgalasko 😲 this is super cool! cc @jessesquires

@Przemyslaw-Wosko
Copy link

Przemyslaw-Wosko commented Mar 5, 2017

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

@danielgalasko
Copy link

danielgalasko commented Mar 7, 2017

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
Copy link

Eke commented Apr 13, 2017

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

@danielgalasko
Copy link

@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
Copy link

vibrazy commented Jun 6, 2017

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
Copy link

tunidev commented Oct 10, 2017

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

@levi
Copy link
Contributor

levi commented Nov 11, 2017

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
Copy link

tunidev commented Jan 17, 2018

@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
Copy link
Contributor Author

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
Copy link
Contributor

levi commented Jan 18, 2018

Thanks for the update, Ryan!

@JUSTINMKAUFMAN
Copy link

@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
Copy link

Arcovv commented Apr 13, 2018

@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
Copy link

matthewweldon commented May 29, 2018

@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
Copy link

Any progress officially supporting this? @rnystrom

@claudiogomezGL
Copy link

@rnystrom Any progress supporting this?

@jonathansolorzn
Copy link

2019 Already, is this still a WIP?

@iwasrobbed-ks
Copy link

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

@wonder2011
Copy link

wonder2011 commented Jul 8, 2022

Is 2022. Nothing yet ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests