New Provider
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()
)
@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.
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])
How does this final code perform the refresh and assignment operations How to use multiple view in box Provider