Skip to content

New Provider #118

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

Open
dillidon opened this issue Nov 30, 2018 · 3 comments
Open

New Provider #118

dillidon opened this issue Nov 30, 2018 · 3 comments

Comments

@dillidon
Copy link

dillidon commented Nov 30, 2018

Hi!

There is a lack of a provider that could contain a data with different types of views, and not just one type.

For example, you can combine view and size w/o data into one box.

public protocol BoxType {
    
    var id: String { get set }
    func view() -> UIView
    func update(view: UIView)
    func size(at index: Int, collectionSize: CGSize) -> CGSize
    func didTap(view: UIView, at index: Int)
}

public class Box<View>: BoxType, CollectionReloadable where View: UIView {
    
    public typealias ViewGenerator = () -> View
    public typealias ViewUpdater = (View) -> Void
    public typealias SizeGenerator = (Int, CGSize) -> CGSize
    public typealias TapHandler = (View, Int) -> Void
    
    public private(set) lazy var reuseManager = CollectionReuseViewManager()
    
    public var id: String
    public var viewGenerator: ViewGenerator?
    public var viewUpdater: ViewUpdater?
    public var sizeSource: SizeGenerator?
    public var tapHandler: TapHandler?
    
    public init(id: String = UUID().uuidString) {
        self.id = id
    }
    
    @discardableResult func view(new: @escaping ViewGenerator) -> Self {
        viewGenerator = new
        return self
    }
    
    @discardableResult func update(new: @escaping ViewUpdater) -> Self {
        viewUpdater = new
        return self
    }
    
    @discardableResult func size(new: @escaping SizeGenerator) -> Self {
        sizeSource = new
        return self
    }
    
    @discardableResult func tap(new: @escaping TapHandler) -> Self {
        tapHandler = new
        return self
    }
    
    public func view() -> UIView {
        if let viewGenerator = viewGenerator {
            let view = reuseManager.dequeue(viewGenerator())
            update(view: view)
            return view
        } else {
            let view = reuseManager.dequeue(View())
            update(view: view)
            return view
        }
    }
    
    public func update(view: UIView) {
        guard let view = view as? View else { return }
        viewUpdater?(view)
    }
    
    open func size(at index: Int, collectionSize: CGSize) -> CGSize {
        return sizeSource?(index, collectionSize) ?? collectionSize
    }
    
    open func didTap(view: UIView, at index: Int) {
        guard let tapHandler = tapHandler, let view = view as? View else { return }
        tapHandler(view, index)
    }
}

open class BoxDataSource: CollectionReloadable {
    
    open var data: [BoxType] { didSet { setNeedsReload() } }
    
    public init(data: [BoxType] = []) {
        self.data = data
    }
    
    open var numberOfItems: Int {
        return data.count
    }
    
    open func identifier(at: Int) -> String {
        return data[at].id
    }
    
    open func data(at: Int) -> BoxType {
        return data[at]
    }
}


open class BoxProvider: ItemProvider, LayoutableProvider, CollectionReloadable {
    
    open var identifier: String?
    open var dataSource: BoxDataSource {
        didSet {
            setNeedsReload()
            setNeedsInvalidateLayout()
        }
    }
    open var layout: Layout { didSet { setNeedsInvalidateLayout() } }
    open var animator: Animator? { didSet { setNeedsReload() } }
    
    public init(identifier: String? = nil,
                dataSource: BoxDataSource,
                layout: Layout = FlowLayout(),
                animator: Animator? = nil) {
        self.identifier = identifier
        self.dataSource = dataSource
        self.layout = layout
        self.animator = animator
    }
    
    open var numberOfItems: Int {
        return dataSource.numberOfItems
    }
    open func view(at index: Int) -> UIView {
        return dataSource.data(at: index).view()
    }
    open func update(view: UIView, at index: Int) {
        dataSource.data(at: index).update(view: view)
    }
    open func identifier(at: Int) -> String {
        return dataSource.identifier(at: at)
    }
    open func layoutContext(collectionSize: CGSize) -> LayoutContext {
        return NewProviderLayoutContext(collectionSize: collectionSize, dataSource: dataSource)
    }
    open func animator(at: Int) -> Animator? {
        return animator
    }
    open func didTap(view: UIView, at: Int) {
        dataSource.data(at: at).didTap(view: view, at: at)
    }
    open func hasReloadable(_ reloadable: CollectionReloadable) -> Bool {
        return reloadable === self || reloadable === dataSource //|| reloadable === sizeSource
    }
}

struct NewProviderLayoutContext: LayoutContext {
    var collectionSize: CGSize
    var dataSource: BoxDataSource
    
    var numberOfItems: Int {
        return dataSource.numberOfItems
    }
    func data(at: Int) -> Any {
        return dataSource.data(at: at)
    }
    func identifier(at: Int) -> String {
        return dataSource.identifier(at: at)
    }
    func size(at index: Int, collectionSize: CGSize) -> CGSize {
        return dataSource.data(at: index).size(at: index, collectionSize: collectionSize)
    }
}

This would make it much easier to create such examples as messages). I tested this code and it works with your example 'HorizontalGalleryViewController'.

let dataSource = BoxDataSource(data: testImages.map { image in
        Box<UIImageView>()
            .view {
                let view = UIImageView()
                view.layer.cornerRadius = 5
                view.clipsToBounds = true
                return view
            }
            .update { view in
                view.image = image
            }
            .size { index, collectionSize in
                var imageSize = image.size
                if imageSize.width > collectionSize.width {
                    imageSize.height /= imageSize.width / collectionSize.width
                    imageSize.width = collectionSize.width
                }
                if imageSize.height > collectionSize.height {
                    imageSize.width /= imageSize.height / collectionSize.height
                    imageSize.height = collectionSize.height
                }
                return imageSize
        }
    })
    provider = BoxProvider(
        dataSource: dataSource,
        layout: WaterfallLayout(columns: 2, spacing: 10).transposed().insetVisibleFrame(by: visibleFrameInsets),
        animator: WobbleAnimator()
    )
@lkzhao
Copy link
Collaborator

lkzhao commented Dec 13, 2018

@dillidon Yes, actually this sounds great!
I would like to further discuss this. Your proposal looks nice, it does solve the problem of having multiple view types nicely. But I wonder if we can get it as the default and make it work with the rest of the system.

The only drawback with this is that it adds another layer of complexity to the user. there is ComposedProvider <- BoxProvider <- Box all feed into one another. So here is what I'm thinking if a Box and provide a section as well and BoxProvider basically takes on the job of the ComposedProvider. It would be really nice.

@dillidon
Copy link
Author

dillidon commented Mar 11, 2019

finally, if you use this code:

public protocol BoxBase: CollectionReloadable { }

public protocol BoxType: BoxBase {
    
    var id: String { get set }
    func view() -> UIView
    func update(view: UIView, provider: BoxProvider)
    func size(at index: Int, collectionSize: CGSize, provider: BoxProvider) -> CGSize
    func didTap(view: UIView, at index: Int, provider: BoxProvider)
}

public extension Array where Element == BoxType {
    
    public func index(of item: Element) -> Int? {
        return index(id: item.id)
    }
    
    public func index(id: String) -> Int? {
        print(self.map {$0.id})
        for index in 0 ..< self.count {
            if self[index].id == id {
                return index
            }
        }
        return nil
    }
    
    public func item(at index: Int) -> Element? {
        guard index >= 0 && index < count else { return nil }
        return self[index]
    }
}

public struct BoxViewContext<View: UIView> {
    public var view: View
    public var provider: BoxProvider
    public var base: BoxBase
}

public struct BoxSizeContext<View: UIView> {
    public var view: () -> View
    public var index: Int
    public var contentSize: CGSize
}

public struct BoxTapContext<View: UIView> {
    public var index: Int
    public var view: View
    public var provider: BoxProvider
    public var base: BoxBase
}

public class Box<View: UIView>: BoxType {
    
    public typealias ViewSource = (BoxViewContext<View>) -> Void
    public typealias SizeSource = (BoxSizeContext<View>) -> CGSize
    public typealias TapHandler = (BoxTapContext<View>) -> Void
    
    public private(set) lazy var reuseManager = CollectionReuseViewManager()
    
    public var id: String
    internal var viewSource: ViewSource?
    internal var sizeSource: SizeSource?
    internal var tapHandler: TapHandler?
    
    public init(id: String = UUID().uuidString) {
        self.id = id
    }
    
    @discardableResult public func view(new: @escaping ViewSource) -> Self {
        viewSource = new
        return self
    }
    
    @discardableResult public func size(new: @escaping SizeSource) -> Self {
        sizeSource = new
        return self
    }
    
    @discardableResult public func tap(new: @escaping TapHandler) -> Self {
        tapHandler = new
        return self
    }
    
    public func view() -> UIView {
        return reuseManager.dequeue(View())
    }
    
    public func update(view: UIView, provider: BoxProvider) {
        guard let view = view as? View else { return }
        let context = BoxViewContext(view: view, provider: provider, base: self)
        viewSource?(context)
    }
    
    public func size(at index: Int, collectionSize: CGSize, provider: BoxProvider) -> CGSize {
        let view: () -> View = { [unowned self] in
            guard let view = self.view() as? View else {
                print("!!! can't get view in BOX")
                return View()
            }
            self.update(view: view, provider: provider)
            return view
        }
        let context = BoxSizeContext(view: view, index: index, contentSize: collectionSize)
        return sizeSource?(context) ?? collectionSize
    }
    
    public func didTap(view: UIView, at index: Int, provider: BoxProvider) {
        guard let tapHandler = tapHandler, let view = view as? View else { return }
        let context = BoxTapContext(index: index, view: view, provider: provider, base: self)
        tapHandler(context)
    }
}

open class BoxDataSource: CollectionReloadable {
    
    open var data: [BoxType] { didSet { setNeedsReload() } }
    
    public init(data: [BoxType] = []) {
        self.data = data
    }
    
    public init(data: BoxType) {
        self.data = [data]
    }
    
    open var numberOfItems: Int {
        return data.count
    }
    
    open func identifier(at: Int) -> String {
        return data[at].id
    }
    
    open func data(at: Int) -> BoxType {
        return data[at]
    }
}


open class BoxProvider: ItemProvider, LayoutableProvider, CollectionReloadable {
    
    open var identifier: String?
    open var data: [BoxType] {
        didSet {
            setNeedsReload()
            setNeedsInvalidateLayout()
        }
    }
    open var layout: Layout { didSet { setNeedsInvalidateLayout() } }
    open var animator: Animator? { didSet { setNeedsReload() } }
    
    public init(id: String? = nil,
                data: [BoxType] = [],
                layout: Layout = FlowLayout(),
                animator: Animator? = nil) {
        self.identifier = id
        self.data = data
        self.layout = layout
        self.animator = animator
    }
    open func identifier(at index: Int) -> String {
        return data[index].id
    }
    open var numberOfItems: Int {
        return data.count
    }
    open func view(at index: Int) -> UIView {
        return data[index].view()
    }
    open func update(view: UIView, at index: Int) {
        data[index].update(view: view, provider: self)
    }
    open func layoutContext(collectionSize: CGSize) -> LayoutContext {
        return BoxProviderLayoutContext(collectionSize: collectionSize, provider: self, data: data)
    }
    open func animator(at: Int) -> Animator? {
        return animator
    }
    open func didTap(view: UIView, at index: Int) {
        data[index].didTap(view: view, at: index, provider: self)
    }
    open func hasReloadable(_ reloadable: CollectionReloadable) -> Bool {
        return reloadable === self //|| reloadable === data //|| reloadable === sizeSource
    }
}

extension BoxProvider {
    
    open func remove(item id: String) {
        guard let index = data.index(id: id) else { return }
        data.remove(at: index)
    }
    open func replace(item id: String, with new: BoxType) {
        guard let index = data.index(id: id) else { return }
        data[index] = new
    }
    open func replace(item old: BoxType, with new: BoxType) {
        guard let index = data.index(of: old) else { return }
        data[index] = new
    }
    open func replace(item old: BoxType, with new: [BoxType]) {
        guard let index = data.index(of: old) else { return }
        data.remove(at: index)
        for (i, box) in new.enumerated() {
            data.insert(box, at: index + i)
        }
    }
}

struct BoxProviderLayoutContext: LayoutContext {
    var collectionSize: CGSize
    var provider: BoxProvider
    var data: [BoxType]
    
    var numberOfItems: Int {
        return data.count
    }
    func data(at: Int) -> Any {
        return data[at]
    }
    func identifier(at: Int) -> String {
        return data[at].id
    }
    func size(at index: Int, collectionSize: CGSize) -> CGSize {
        return data[index].size(at: index, collectionSize: collectionSize, provider: provider)
    }
}

open class BoxComposedProvider: SectionProvider, LayoutableProvider, CollectionReloadable {
    
    open var identifier: String?
    open var providers: [BoxProvider] { didSet { setNeedsReload() } }
    open var layout: Layout { didSet { setNeedsInvalidateLayout() } }
    open var animator: Animator? { didSet { setNeedsReload() } }
    
    public init(id: String? = nil,
                layout: Layout = FlowLayout(),
                animator: Animator? = nil,
                providers: [BoxProvider] = []) {
        self.identifier = id
        self.layout = layout
        self.animator = animator
        self.providers = providers
    }
    open func identifier(at index: Int) -> String {
        return providers[index].identifier ?? "\(index)"
    }
    open var numberOfItems: Int {
        return providers.count
    }
    open func section(at index: Int) -> Provider? {
        return providers[index]
    }
    open func layoutContext(collectionSize: CGSize) -> LayoutContext {
        return BoxComposedProviderLayoutContext(collectionSize: collectionSize, providers: providers)
    }
    open func animator(at: Int) -> Animator? {
        return animator
    }
    open func willReload() {
        for provider in providers {
            provider.willReload()
        }
    }
    
    open func didReload() {
        for provider in providers {
            provider.didReload()
        }
    }
    open func hasReloadable(_ reloadable: CollectionReloadable) -> Bool {
        return reloadable === self || providers.contains(where: { $0.hasReloadable(reloadable) })
    }
}

struct BoxComposedProviderLayoutContext: LayoutContext {
    var collectionSize: CGSize
    var providers: [BoxProvider]
    
    var numberOfItems: Int {
        return providers.count
    }
    func data(at: Int) -> Any {
        return providers[at]
    }
    func identifier(at: Int) -> String {
        return providers[at].identifier ?? "\(at)"
    }
    func size(at index: Int, collectionSize: CGSize) -> CGSize {
        providers[index].layout(collectionSize: collectionSize)
        return providers[index].contentSize
    }
}

with this code:

struct ViewConfig<T> {
    let config: (T) -> Void
}

protocol Configurable {
    init()
}

extension UIView: Configurable {}

extension Configurable {
    
    init(config: ViewConfig<Self>) {
        self.init()
        apply(config: config)
    }
    
    @discardableResult
    func apply(config: ViewConfig<Self>) -> Self {
        config.config(self)
        return self
    }
    
    @discardableResult
    func config(view: (Self) -> Void) -> Self {
        view(self)
        return self
    }
}

you can implement as follows

extension ViewConfig where T: UITextField {
    
    static var name: ViewConfig<UITextField> {
        return ViewConfig<UITextField> {
            $0.placeholder = Localized.NAME
            $0.font = font
            $0.backgroundColor = nil
            $0.textColor = .black
            $0.clearButtonMode = .whileEditing
            $0.autocapitalizationType = .words
            $0.keyboardType = .default
            $0.spellCheckingType = .no
            $0.returnKeyType = .next
        }
    }
}

let data = Box<UITextField>()
                .view { $0.view.apply(config: .name) }
                .tap { $0.view.becomeFirstResponder() }
                .size { CGSize(width: $0.contentSize.width, height: 57) }

let provider = BoxProvider(data: [data])

@SpectatorNan
Copy link

How does this final code perform the refresh and assignment operations
How to use multiple view in box Provider

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

No branches or pull requests

3 participants