ASMR/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt

189 lines
5.7 KiB
Kotlin

package net.shadowfacts.cacao.viewcontroller
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.util.properties.ObservableLazyProperty
import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
import java.lang.RuntimeException
import java.util.*
/**
* The base Cacao View Controller class. A view controller is an object that owns and manages a [View].
*
* The view controller receives lifecycle callbacks for its view.
*
* @author shadowfacts
*/
abstract class ViewController {
/**
* The window that contains this view controller.
* This property is not set until either:
* a) a [Window] is initialized with this VC as it's root view controller or
* b) this VC is added as a child of another view controller.
*/
var window: Window? = null
/**
* Helper function for creating layout constraints in the domain of this VC's window.
* This function is not usable until [window] is initialized.
*/
val createConstraints
get() = window!!.solver::dsl
/**
* The view that this View Controller has.
* This property is created by [loadView] and is not initialized before that method has been called.
*
* @see loadView
*/
lateinit var view: View
protected set
/**
* This VC's parent view controller. If `null`, this VC is the root view controller of its [window].
*/
var parent: ViewController? = null
set(value) {
willMoveTo(value)
field = value
didMoveTo(value)
}
// _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods
private var _children = LinkedList<ViewController>()
/**
* The list of all the child VCs of this view controller.
* This list should never be mutated directly, only by the [embedChild]/[removeChild] methods.
*/
val children: List<ViewController> = _children
/**
* This method somehow loads a [View] and sets this VC's [view] property to it.
*
* This method should only be called by the framework. After the [view] property is set, the framework is
* responsible for initializing its [View.window]/[View.solver] properties and calling [View.wasAdded].
*/
abstract fun loadView()
/**
* This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been
* called.
*/
open fun viewDidLoad() {}
/**
* This method is called immediately before the [Window.solver] is going to solve constraints and update variables.
* If overridden, the superclass method must be called.
*/
open fun viewWillLayoutSubviews() {
children.forEach(ViewController::viewWillLayoutSubviews)
}
/**
* This method is called immediately after the [Window.solver] has solved constraints and variables have been updated.
* This method is responsible for invoking the VC's [View.didLayout] method.
* If overridden, the superclass method must be called.
*/
open fun viewDidLayoutSubviews() {
view.didLayout()
children.forEach(ViewController::viewDidLayoutSubviews)
}
/**
* Called when the VC's view has been added to the screen and is about to be displayed.
*/
open fun viewWillAppear() {
children.forEach(ViewController::viewWillAppear)
}
/**
* Called immediately after the VC's view has first been displayed on screen.
*/
open fun viewDidAppear() {
children.forEach(ViewController::viewDidAppear)
}
/**
* Called before the view will disappear from the screen, either because the VC has been removed from it's parent/screen
* or because the [net.shadowfacts.cacao.CacaoScreen] has been closed.
*/
open fun viewWillDisappear() {
children.forEach(ViewController::viewWillDisappear)
}
/**
* Called after the view has disappeared from the screen.
*/
open fun viewDidDisappear() {
children.forEach(ViewController::viewDidDisappear)
}
/**
* Called before the view controller's parent changes to the given new value.
*
* @param parent The new parent view controller.
*/
open fun willMoveTo(parent: ViewController?) {}
/**
* Called after the view controller's parent has changed to the given new value.
*
* @param parent The new parent view controller.
*/
open fun didMoveTo(parent: ViewController?) {}
/**
* Embeds a child view controller in this VC.
*
* @param viewController The new child VC.
* @param container The view that will be used as the superview for the child VC's view. Defaults to this VC's [view].
* @param pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges.
* Defaults to `true`.
*/
fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) {
viewController.parent = this
viewController.window = window
_children.add(viewController)
viewController.loadView()
container.addSubview(viewController.view)
if (pinEdges) {
createConstraints {
viewController.view.leftAnchor equalTo container.leftAnchor
viewController.view.rightAnchor equalTo container.rightAnchor
viewController.view.topAnchor equalTo container.topAnchor
viewController.view.bottomAnchor equalTo container.bottomAnchor
}
}
viewController.viewDidLoad()
}
/**
* Removes the given view controller
*
* @param viewController The child VC to remove from this view controller.
* @throws RuntimeException If the given [viewController] is not a child of this VC.
*/
fun removeChild(viewController: ViewController) {
if (viewController.parent != this) {
throw RuntimeException("Cannot remove child view controller whose parent is not this view controller")
}
viewController.parent = null
_children.remove(viewController)
}
/**
* Removes this view controller from its parent, if it has one.
*/
fun removeFromParent() {
parent?.removeChild(this)
view.removeFromSuperview()
// todo: remove view from superview
}
}