Created
May 11, 2019 22:06
-
-
Save kaqu/0045e632be7b27072c42617ecbbe18eb to your computer and use it in GitHub Desktop.
FluidLayout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public protocol FluidView: UIView { | |
var id: UUID { get } | |
} | |
public protocol FluidViewLayoutModel { | |
var id: UUID { get } | |
var prefferedSize: CGSize? { get } | |
var prefferedInsets: UIEdgeInsets? { get } | |
var minimumSize: CGSize? { get } | |
var maximumSize: CGSize? { get } | |
func prepareView() -> FluidView | |
func updateView(_ view: FluidView) -> Void | |
} | |
public struct FluidViewLayoutState { | |
public let viewID: UUID | |
public var frame: CGRect | |
public init(viewID: UUID, frame: CGRect = .zero) { | |
self.viewID = viewID | |
self.frame = frame | |
} | |
} | |
public protocol FluidLayoutContext { | |
var containerFrame: CGRect { get } | |
var containerInsets: UIEdgeInsets { get } | |
var previousFrame: CGRect { get set } | |
init(containerFrame: CGRect, containerInsets: UIEdgeInsets, model: [FluidViewLayoutModel]) | |
} | |
public struct FluidLayouting<Context: FluidLayoutContext> { | |
public var layout: (inout Context, FluidViewLayoutModel) -> FluidViewLayoutState | |
public init(layout: @escaping (inout Context, FluidViewLayoutModel) -> FluidViewLayoutState) { | |
self.layout = layout | |
} | |
} | |
public final class FluidLayoutContainer<LayoutContext: FluidLayoutContext>: UIView, FluidView { | |
public let id: UUID | |
private let layout: FluidLayouting<LayoutContext> | |
private var managedViews: [UUID:FluidView] = [:] | |
public var managedViewModels: [FluidViewLayoutModel] = [] { | |
willSet { | |
let newKeys: [UUID] = newValue.map { $0.id } | |
managedViews.keys | |
.filter { !newKeys.contains($0) } | |
.forEach { | |
managedViews[$0]?.removeFromSuperview() | |
managedViews[$0] = nil | |
} | |
newValue | |
.filter { managedViews.keys.contains($0.id) } | |
.forEach { | |
guard let view = managedViews[$0.id] else { return } | |
$0.updateView(view) | |
} | |
newValue | |
.filter { !managedViews.keys.contains($0.id) } | |
.forEach { | |
let newView = $0.prepareView() | |
managedViews[$0.id] = newView | |
addSubview(newView) | |
} | |
} | |
didSet { setNeedsLayout() } | |
} | |
public init(id: UUID = .init(), layout: FluidLayouting<LayoutContext>) { | |
self.id = id | |
self.layout = layout | |
super.init(frame: .zero) | |
} | |
@available(*, unavailable) | |
required public init?(coder aDecoder: NSCoder) { fatalError() } | |
public override func layoutSubviews() { | |
var ctx: LayoutContext = .init(containerFrame: bounds, containerInsets: .zero, model: managedViewModels) | |
managedViewModels | |
.map { layout.layout(&ctx, $0) } | |
.forEach { | |
guard let view = managedViews[$0.viewID] else { return } | |
view.frame = $0.frame | |
view.layoutSubviews() | |
} | |
} | |
} | |
/// SAMPLE | |
public struct BoxLayoutContext: FluidLayoutContext { | |
public var containerFrame: CGRect | |
public var containerInsets: UIEdgeInsets | |
public var previousFrame: CGRect = .zero | |
public var counter: UInt = 0 | |
public init(containerFrame: CGRect, containerInsets: UIEdgeInsets, model: [FluidViewLayoutModel]) { | |
self.containerFrame = containerFrame | |
self.containerInsets = containerInsets | |
} | |
} | |
extension FluidLayouting where Context == BoxLayoutContext { | |
public static var columnLayout: FluidLayouting { | |
return .init(layout: { (ctx, model) -> FluidViewLayoutState in | |
let size: CGSize = model.prefferedSize ?? .zero | |
let frame: CGRect = .init(x: ctx.containerInsets.left, | |
y: ctx.counter == 0 ? ctx.containerInsets.top : ctx.previousFrame.origin.y + ctx.previousFrame.height, | |
width: ctx.containerFrame.size.width - ctx.containerInsets.left - ctx.containerInsets.right, | |
height: size.height) | |
ctx.previousFrame = frame | |
ctx.counter += 1 | |
return FluidViewLayoutState(viewID: model.id, frame: frame) | |
}) | |
} | |
} | |
extension FluidLayouting where Context == BoxLayoutContext { | |
public static var rowLayout: FluidLayouting { | |
return .init(layout: { (ctx, model) -> FluidViewLayoutState in | |
let size: CGSize = model.prefferedSize ?? .zero | |
let frame: CGRect = .init(x: ctx.counter == 0 ? ctx.containerInsets.left : ctx.previousFrame.origin.x + ctx.previousFrame.width, | |
y: ctx.containerInsets.top, | |
width: size.width, | |
height: ctx.containerFrame.size.height - ctx.containerInsets.top - ctx.containerInsets.bottom) | |
ctx.previousFrame = frame | |
ctx.counter += 1 | |
return FluidViewLayoutState(viewID: model.id, frame: frame) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment