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

How to mix data/object types #38

Closed
hiroshihorie opened this issue Dec 21, 2017 · 7 comments
Closed

How to mix data/object types #38

hiroshihorie opened this issue Dec 21, 2017 · 7 comments

Comments

@hiroshihorie
Copy link
Contributor

In a realistic case where the collection requires many different data/object types, how to implement this ?
For example a settings page may require TextAndSwitchView, TextAndSliderView, LongTextView and so on..

if there was something like...
provider.viewClassForData = { dataObj in
return dataObj.viewClass
}

@lkzhao
Copy link
Collaborator

lkzhao commented Dec 21, 2017

To support this, right now you have to implement a custom view provider and make the generic ViewType to be UIView.

It will look something like:

enum SettingsDataType {
  case textAndSwitch(text: String)
  case textAndSlider(text: String)
  case longText(text: String)
}

class SettingsViewProvider: CollectionViewProvider<SettingsDataType, UIView> {
  override func update(view: UIView, data: SettingsDataType, index: Int) {
    switch data {
    case .textAndSwitch(let text):
      (view as? TextAndSwitchView)?.text = text
    case .textAndSlider(let text):
      (view as? TextAndSliderView)?.text = text
    case .longText(let text):
      (view as? LongTextView)?.text = text
    }
  }

  override func view(data: SettingsDataType, index: Int) -> UIView {
    let view: UIView
    switch data {
    case .textAndSwitch(let text):
      view = reuseManager.dequeue(TextAndSwitchView())
    case .textAndSlider(let text):
      view = reuseManager.dequeue(TextAndSliderView())
    case .longText(let text):
      view = reuseManager.dequeue(LongTextView())
    }
    update(view: view, data: data, index: index)
    return view
  }
}

provider = CollectionProvider(data: [.textAndSwitch(text: "ABC")], viewProvider: SettingsViewProvider())

This is definitely something that needs some improvement. with this workaround we lost the view type information and cannot benefit from swift's generic.

It would be best if we can utilize swift generics and allow the compiler to guarantee view types. I will think about it more. let me know if you have other ideas.

@hiroshihorie
Copy link
Contributor Author

hiroshihorie commented Dec 22, 2017

Hello lkzhao, thanks for your reply. Your projects are great, I also use your Hero library.
Although not the cleanest, for now, I will use the following design (for a single column collection)

class BaseObj {
    // Object responsible for specifying view
    var view: BaseView.Type { return BaseView.self }
    var insets = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)
}

class BaseView: UIView {
    weak var data: AnyObject?
    
    func commonInit() { }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func set(_ data: AnyObject?) {
        self.data = data
    }
    
    // View responsible for specifying height
    class func height(data: AnyObject?, width: CGFloat) -> CGFloat {
        return 100
    }
}

class BaseViewProvider: CollectionViewProvider<BaseObj, BaseView> {
    
    override func update(view: BaseView, data: BaseObj, index: Int) {
        view.set(data)
    }
    
    override func view(data: BaseObj, index: Int) -> BaseView {
        let view = reuseManager.dequeue(data.view.init())
        update(view: view, data: data, index: index)
        return view
    }
    
}

let BaseSizeProvider: CollectionSizeProvider<BaseObj> = { (index, data, collectionSize) in
    let height = data.view.height(data: data, width: collectionSize.width);
    return CGSize(width: collectionSize.width, height: height)
}

// MARK : Label

class LabelObj: BaseObj {
    override var view: BaseView.Type { return LabelView.self }
    var text: String
    var font = UIFont.systemFont(ofSize: 16)
    init(text: String) { self.text = text }
}

class LabelView: BaseView {
    var label = UILabel()
    
    override func commonInit() {
        backgroundColor = .clouds
        label.numberOfLines = 0
        label.textAlignment = .center
        addSubview(label)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if let data = data as? LabelObj {
            label.frame = UIEdgeInsetsInsetRect(bounds, data.insets)
        }
    }
    
    override func set(_ data: AnyObject?) {
        super.set(data)
        if let data = data as? LabelObj {
            label.font = data.font
            label.text = data.text
        }
    }
    
    override class func height(data: AnyObject?, width: CGFloat) -> CGFloat {
        if let data = data as? LabelObj {
            let horizontalInsets = data.insets.left + data.insets.right
            let verticalInsets = data.insets.top + data.insets.bottom
            return data.text.height(width: width - horizontalInsets, font: data.font) + verticalInsets
        }
        return 100
    }
}

// ... Create more Obj/View pairs ...

extension String {
    
    func height(width: CGFloat, font : UIFont)->CGFloat {
        
        let paragraph = NSMutableParagraphStyle()
        paragraph.lineBreakMode = .byWordWrapping
        
        return self.boundingRect(
            with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: [.font: font, .paragraphStyle: paragraph],
            context: nil).height
    }
}
let dataProvider = ArrayDataProvider<BaseObj>(data: [
    LabelObj(text: "This is a text object"),
    LabelObj(text: "This is some long text, This is some long text, This is some long text, This is some long text, This is some long text, This is some long text, This is some long text."),
])

let provider = CollectionProvider<BaseObj, BaseView>(
    dataProvider: dataProvider,
    viewProvider: BaseViewProvider(),
    sizeProvider: BaseSizeProvider
)

This design requires casting of data in the View class which is ugly, any ideas to avoid this ?

if let data = data as? LabelObj {
}

Another questions is that the reuseManager.dequeue requires the view to be instantiated, resulting the view to be initialized every time. Isn't it better to pass the Type and let the reuseManager decide when to instantiate the view ? Sorry if I'm missing something.

I'm new to swift so please let me know if you have better ideas.

@lkzhao
Copy link
Collaborator

lkzhao commented Dec 22, 2017

reuseManager.dequeue uses Swift's autoclosure so that the code is not actually executed if there is a reusable cell. see https://github.com/SoySauceLab/CollectionKit/blob/master/Sources/Other/CollectionReuseViewManager.swift#L35

For your case, It doesn't reuse because the compiler doesn't know what kind of cell you are trying to dequeue and it will probably try to dequeue a base UIView instead of LabelView. The reuseManager won't be able to reuse the LabelView that you have queued before.

But like you said we should allow user to pass in a view type to dequeue a cell.

If you have the time do you want to take a look at adding that extra dequeue method for the reuseManager?

@lkzhao
Copy link
Collaborator

lkzhao commented Dec 22, 2017

The casting is not easily avoidable with the current design. But I'm thinking about giving CollectionProvider the ability to take in multiple ViewProvider and a mapper function that determines which ViewProvider to use for which type of data.

@hiroshihorie
Copy link
Contributor Author

hiroshihorie commented Dec 22, 2017

I just learned autoclosure, delayed execution, clever.
I have implemented the dequeuing by Type #39

@lkzhao
Copy link
Collaborator

lkzhao commented Jun 23, 2018

In v2.0, there is a ComposedViewSource that allows you to give a viewSourceSelector function to determine the actual view source matching a particular type of data. The casting is done automatically for you.

let labelViewSource = ClosureViewSource(viewUpdater: { 
  (view: UILabel, data: LabelObj, at: Int) in
  view.font = data.font
  view.text = data.text
})
let imageViewSource = ClosureViewSource(viewUpdater: { 
  (view: UIImageView, data: ImageObj, at: Int) in
  view.image = data.image
})

let dataSource = ArrayDataSource(data: [
  LabelObj(text: "This is a text object"),
  LabelObj(text: "This is another text object"),
  ImageObj(image: UIImage(named: "someImage"))
])

let viewSource = ComposedViewSource(viewSourceSelector: { data in
  if data is LabelObj {
    return labelViewSource
  } else if data is ImageObj {
    return imageViewSource
  } else {
    fatalError("Unsupported data type: \(data)")
  }
})

let provider = BasicProvider(
  dataSource: dataSource,
  viewSource: viewSource
)

@lkzhao lkzhao closed this as completed Jun 23, 2018
@hiroshihorie
Copy link
Contributor Author

Thanks @lkzhao , for both implementing and letting me know.
I will eventually migrate to your design.

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

2 participants