Skip to content

Commit cb45031

Browse files
committed
#10 Support for lightweight views without associated view controllers
1 parent 41225a9 commit cb45031

File tree

4 files changed

+229
-80
lines changed

4 files changed

+229
-80
lines changed

Sources/ScrollStackController/ScrollStack.swift

Lines changed: 183 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
241241

242242
// MARK: - Set Rows
243243

244-
/// Remove all existing rows and setup the new rows.
244+
/// Remove all existing rows and put in place the new list based upon passed controllers.
245245
///
246246
/// - Parameter controllers: controllers to set.
247247
@discardableResult
@@ -250,7 +250,43 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
250250
return addRows(controllers: controllers)
251251
}
252252

253+
/// Remove all existing rows and put in place the new list based upon passed views.
254+
///
255+
/// - Parameter views: views to set.
256+
open func setRows(views: [UIView]) -> [ScrollStackRow] {
257+
removeAllRows(animated: false)
258+
return addRows(views: views)
259+
}
260+
253261
// MARK: - Insert Rows
262+
263+
/// Insert a new to manage passed view without associated controller.
264+
///
265+
/// - Parameters:
266+
/// - view: view to add. It will be added as contentView of the row.
267+
/// - location: location inside the stack of the new row.
268+
/// - animated: `true` to animate operation, by default is `false`.
269+
/// - completion: completion: optional completion callback to call at the end of insertion.
270+
open func addRow(view: UIView, at location: InsertLocation = .bottom, animated: Bool = false, completion: (() -> Void)? = nil) -> ScrollStackRow? {
271+
guard let index = indexForLocation(location) else {
272+
return nil
273+
}
274+
275+
return createRowForView(view, insertAt: index, animated: animated, completion: completion)
276+
}
277+
278+
/// Add new rows for each passed view.
279+
///
280+
/// - Parameter controllers: controllers to add as rows.
281+
/// - Parameter location: location inside the stack of the new row.
282+
/// - Parameter animated: `true` to animate operatio, by default is `false`.
283+
open func addRows(views: [UIView], at location: InsertLocation = .bottom, animated: Bool = false) -> [ScrollStackRow] {
284+
enumerateItems(views, insertAt: location) {
285+
addRow(view: $0, at: location, animated: animated)
286+
}
287+
}
288+
289+
254290
/// Insert a new row to manage passed controller instance.
255291
///
256292
/// - Parameter controller: controller to manage; it's `view` will be added as contentView of the row.
@@ -259,52 +295,22 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
259295
/// - Parameter completion: optional completion callback to call at the end of insertion.
260296
@discardableResult
261297
open func addRow(controller: UIViewController, at location: InsertLocation = .bottom, animated: Bool = false, completion: (() -> Void)? = nil) -> ScrollStackRow? {
262-
switch location {
263-
case .top:
264-
return createRowForController(controller, insertAt: 0, animated: animated, completion: completion)
265-
266-
case .bottom:
267-
return createRowForController(controller, insertAt: rows.count, animated: animated, completion: completion)
268-
269-
case .atIndex(let index):
270-
return createRowForController(controller, insertAt: index, animated: animated, completion: completion)
271-
272-
case .after(let afterController):
273-
guard let index = rowForController(afterController)?.index else {
274-
return nil
275-
}
276-
277-
let finalIndex = ((index + 1) >= rows.count ? rows.count : (index + 1))
278-
return createRowForController(controller, insertAt: finalIndex, animated: animated, completion: completion)
279-
280-
case .before(let beforeController):
281-
guard let index = rowForController(beforeController)?.index else {
282-
return nil
283-
}
284-
285-
return createRowForController(controller, insertAt: index, animated: animated, completion: completion)
286-
298+
guard let index = indexForLocation(location) else {
299+
return nil
287300
}
301+
302+
return createRowForController(controller, insertAt: index, animated: animated, completion: completion)
288303
}
289304

290-
/// Add new rows for each passaed controllers.
305+
/// Add new rows for each passed controllers.
291306
///
292307
/// - Parameter controllers: controllers to add as rows.
293308
/// - Parameter location: location inside the stack of the new row.
294309
/// - Parameter animated: `true` to animate operatio, by default is `false`.
295310
@discardableResult
296311
open func addRows(controllers: [UIViewController], at location: InsertLocation = .bottom, animated: Bool = false) -> [ScrollStackRow] {
297-
switch location {
298-
case .top:
299-
return controllers.reversed().compactMap( {
300-
addRow(controller: $0, at: .top, animated: animated )
301-
}).reversed() // double reversed() is to avoid strange behaviour when additing rows on tops.
302-
303-
default:
304-
return controllers.compactMap {
305-
addRow(controller: $0, at: location, animated: animated)
306-
}
307-
312+
enumerateItems(controllers, insertAt: location) {
313+
addRow(controller: $0, at: location, animated: animated)
308314
}
309315
}
310316

@@ -373,38 +379,32 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
373379
}
374380
}
375381

376-
/// Replace an existing row with another new controller.
382+
/// Replace an existing row with another new row which manage passed view.
383+
///
384+
/// - Parameters:
385+
/// - sourceIndex: row to replace.
386+
/// - view: view to use as `contentView` of the row.
387+
/// - animated: `true` to animate the transition.
388+
/// - completion: optional callback called at the end of the transition.
389+
open func replaceRow(index sourceIndex: Int, withRow view: UIView, animated: Bool = false, completion: (() -> Void)? = nil) {
390+
doReplaceRow(index: sourceIndex, createRow: { (index, animated) -> ScrollStackRow in
391+
return self.createRowForView(view, insertAt: index, animated: animated)
392+
}, animated: animated, completion: completion)
393+
}
394+
395+
/// Replace an existing row with another new row which manage passed controller.
377396
///
378397
/// - Parameter row: row to replace.
379398
/// - Parameter controller: view controller to replace.
380399
/// - Parameter animated: `true` to animate the transition.
381400
/// - Parameter completion: optional callback called at the end of the transition.
382401
open func replaceRow(index sourceIndex: Int, withRow controller: UIViewController, animated: Bool = false, completion: (() -> Void)? = nil) {
383-
guard sourceIndex >= 0, sourceIndex < rows.count else {
384-
return
385-
}
386-
387-
let sourceRow = rows[sourceIndex]
388-
389-
guard animated else {
390-
removeRow(index: sourceRow.index!)
391-
createRowForController(controller, insertAt: sourceIndex, animated: false)
392-
return
393-
}
394-
395-
stackView.setNeedsLayout()
396-
397-
UIView.execute({
398-
sourceRow.isHidden = true
399-
}) {
400-
let newRow = self.createRowForController(controller, insertAt: sourceIndex, animated: false)
401-
newRow.isHidden = true
402-
UIView.execute({
403-
newRow.isHidden = false
404-
}, completion: completion)
405-
}
402+
doReplaceRow(index: sourceIndex, createRow: { (index, animated) in
403+
return self.createRowForController(controller, insertAt: sourceIndex, animated: false)
404+
}, animated: animated, completion: completion)
406405
}
407406

407+
408408
/// Move the row at given index to another index.
409409
/// If one of the indexes is not valid nothing is made.
410410
///
@@ -494,6 +494,19 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
494494
}
495495
}
496496

497+
/// Return the row associated with passed `UIView` instance and its index into the `rows` array.
498+
///
499+
/// - Parameter view: target view (the `contentView` of the associated `ScrollStackRow` instance).
500+
open func rowForView(_ view: UIView) -> (index: Int, cell: ScrollStackRow)? {
501+
guard let index = rows.firstIndex(where: {
502+
$0.contentView == view
503+
}) else {
504+
return nil
505+
}
506+
507+
return (index, rows[index])
508+
}
509+
497510
/// Return the row associated with passed `UIViewController` instance and its index into the `rows` array.
498511
///
499512
/// - Parameter controller: target controller.
@@ -503,6 +516,7 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
503516
}) else {
504517
return nil
505518
}
519+
506520
return (index, rows[index])
507521
}
508522

@@ -626,6 +640,88 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
626640

627641
// MARK: - Private Functions
628642

643+
private func doReplaceRow(index sourceIndex: Int, createRow handler: @escaping ((Int, Bool) -> ScrollStackRow), animated: Bool, completion: (() -> Void)? = nil) {
644+
guard sourceIndex >= 0, sourceIndex < rows.count else {
645+
return
646+
}
647+
648+
let sourceRow = rows[sourceIndex]
649+
guard animated else {
650+
removeRow(index: sourceRow.index!)
651+
_ = handler(sourceIndex, false)
652+
return
653+
}
654+
655+
stackView.setNeedsLayout()
656+
657+
UIView.execute({
658+
sourceRow.isHidden = true
659+
}) {
660+
let newRow = handler(sourceIndex, false)
661+
newRow.isHidden = true
662+
UIView.execute({
663+
newRow.isHidden = false
664+
}, completion: completion)
665+
}
666+
}
667+
668+
/// Enumerate items to insert into the correct order based upon the location of destination.
669+
///
670+
/// - Parameters:
671+
/// - list: list to enumerate.
672+
/// - location: insert location.
673+
/// - callback: callback to call on enumrate.
674+
private func enumerateItems<T>(_ list: [T], insertAt location: InsertLocation, callback: ((T) -> ScrollStackRow?)) -> [ScrollStackRow] {
675+
switch location {
676+
case .top:
677+
return list.reversed().compactMap(callback).reversed() // double reversed() is to avoid strange behaviour when additing rows on tops.
678+
679+
default:
680+
return list.compactMap(callback)
681+
682+
}
683+
}
684+
685+
/// Return the destination index for passed location. `nil` if index is not valid.
686+
///
687+
/// - Parameter location: location.
688+
private func indexForLocation(_ location: InsertLocation) -> Int? {
689+
switch location {
690+
case .top:
691+
return 0
692+
693+
case .bottom:
694+
return rows.count
695+
696+
case .atIndex(let index):
697+
return index
698+
699+
case .after(let controller):
700+
guard let index = rowForController(controller)?.index else {
701+
return nil
702+
}
703+
return ((index + 1) >= rows.count ? rows.count : (index + 1))
704+
705+
case .afterView(let view):
706+
guard let index = rowForView(view)?.index else {
707+
return nil
708+
}
709+
return ((index + 1) >= rows.count ? rows.count : (index + 1))
710+
711+
case .before(let controller):
712+
guard let index = rowForController(controller)?.index else {
713+
return nil
714+
}
715+
return index
716+
717+
case .beforeView(let view):
718+
guard let index = rowForView(view)?.index else {
719+
return nil
720+
}
721+
return index
722+
}
723+
}
724+
629725
/// Initial configuration of the control.
630726
private func setupUI() {
631727
backgroundColor = .white
@@ -730,6 +826,23 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
730826
return removedController
731827
}
732828

829+
/// Create a new row to handle passed view and insert it at specified index.
830+
///
831+
/// - Parameters:
832+
/// - view: view to use as `contentView` of the row.
833+
/// - index: position of the new row with controller's view.
834+
/// - animated: `true` to animate transition.
835+
/// - completion: completion callback called when operation is finished.
836+
@discardableResult
837+
private func createRowForView(_ view: UIView, insertAt index: Int, animated: Bool, completion: (() -> Void)? = nil) -> ScrollStackRow {
838+
// Identify any other cell with the same controller
839+
let cellToRemove = rowForView(view)?.cell
840+
841+
// Create the new container cell for this view.
842+
let newRow = ScrollStackRow(view: view, stackView: self)
843+
return createRow(newRow, at: index, cellToRemove: cellToRemove, animated: animated, completion: completion)
844+
}
845+
733846
/// Create a new row to handle passed controller and insert it at specified index.
734847
///
735848
/// - Parameter controller: controller to manage.
@@ -743,9 +856,16 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate {
743856

744857
// Create the new container cell for this controller's view
745858
let newRow = ScrollStackRow(controller: controller, stackView: self)
859+
return createRow(newRow, at: index, cellToRemove: cellToRemove, animated: animated, completion: completion)
860+
}
861+
862+
/// Private implementation to add new row.
863+
private func createRow(_ newRow: ScrollStackRow, at index: Int,
864+
cellToRemove: ScrollStackRow?,
865+
animated: Bool, completion: (() -> Void)? = nil) -> ScrollStackRow {
746866
onChangeRow?(newRow, false)
747867
stackView.insertArrangedSubview(newRow, at: index)
748-
868+
749869
// Remove any duplicate cell with the same view
750870
removeRowFromStackView(cellToRemove)
751871

0 commit comments

Comments
 (0)