Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions ListableUI/Sources/Decoration/AnyDecoration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// AnyDecoration.swift
// ListableUI
//
// Created by Goose on 7/24/25.
//

import Foundation
import UIKit


public protocol AnyDecoration : AnyDecorationConvertible, AnyDecoration_Internal
{
var anyContent : Any { get }

var sizing : Sizing { get set }
var layouts : DecorationLayouts { get set }

var reappliesToVisibleView: ReappliesToVisibleView { get }
}


public protocol AnyDecoration_Internal
{
var layouts : DecorationLayouts { get }

func apply(
to decorationView : UIView,
for reason : ApplyReason,
with info : ApplyDecorationContentInfo
)

func anyIsEquivalent(to other : AnyDecoration) -> Bool

func newPresentationDecorationState(
kind : SupplementaryKind,
performsContentCallbacks : Bool
) -> Any
}
39 changes: 39 additions & 0 deletions ListableUI/Sources/Decoration/AnyDecorationConvertible.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// AnyDecorationConvertible.swift
// ListableUI
//
// Created by Goose on 7/24/25.
//

import Foundation


/// A type which can be converted into a `Decoration`, so you
/// do not need to explicitly wrap / convert your `DecorationContent`
/// in a `Decoration` when providing a decoration to a list or section:
///
/// ```
/// Section("id") { section in
/// section.decoration = MyDecorationContent(backgroundColor: .red)
/// }
///
/// struct MyDecorationContent : DecorationContent {
/// var backgroundColor : UIColor
/// ...
/// }
/// ```
///
/// Only two types conform to this protocol:
///
/// ### `Decoration`
/// The `Decoration` conformance simply returns self.
///
/// ### `DecorationContent`
/// The `DecorationContent` conformance returns `Decoration(self)`,
/// utilizing the default values from the `Decoration` initializer.
///
public protocol AnyDecorationConvertible {

/// Converts the object into a type-erased `AnyDecoration` instance.
func asAnyDecoration() -> AnyDecoration
}
168 changes: 168 additions & 0 deletions ListableUI/Sources/Decoration/Decoration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// Decoration.swift
// ListableUI
//
// Created by Goose on 7/24/25.
//

import UIKit


public struct Decoration<Content:DecorationContent> : AnyDecoration
{
public var content : Content

public var sizing : Sizing
public var layouts : DecorationLayouts

public typealias OnTap = () -> ()
public var onTap : OnTap?

public var onDisplay : OnDisplay.Callback?
public var onEndDisplay : OnEndDisplay.Callback?

public var debuggingIdentifier : String? = nil

internal let reuseIdentifier : ReuseIdentifier<Content>

//
// MARK: Initialization
//

public typealias Configure = (inout Decoration) -> ()

public init(
_ content : Content,
configure : Configure
) {
self.init(content)

configure(&self)
}

public init(
_ content : Content,
sizing : Sizing? = nil,
layouts : DecorationLayouts? = nil,
onTap : OnTap? = nil,
onDisplay : OnDisplay.Callback? = nil,
onEndDisplay : OnEndDisplay.Callback? = nil
) {
assertIsValueType(Content.self)

self.content = content

let defaults = self.content.defaultDecorationProperties

self.sizing = sizing ?? defaults.sizing ?? .thatFits(.noConstraint)
self.layouts = layouts ?? defaults.layouts ?? .init()
self.onTap = onTap ?? defaults.onTap
self.onDisplay = onDisplay
self.onEndDisplay = onEndDisplay
self.debuggingIdentifier = defaults.debuggingIdentifier

self.reuseIdentifier = ReuseIdentifier.identifier(for: Content.self)
}

// MARK: AnyDecoration

public var anyContent: Any {
self.content
}

public var reappliesToVisibleView: ReappliesToVisibleView {
self.content.reappliesToVisibleView
}

// MARK: AnyDecorationConvertible

public func asAnyDecoration() -> AnyDecoration {
self
}

// MARK: AnyDecoration_Internal

public func apply(
to anyView : UIView,
for reason : ApplyReason,
with info : ApplyDecorationContentInfo
) {
let view = anyView as! DecorationContentView<Content>

let views = DecorationContentViews<Content>(view: view)

self.content.apply(
to: views,
for: reason,
with: info
)
}

public func anyIsEquivalent(to other : AnyDecoration) -> Bool
{
guard let other = other as? Decoration<Content> else {
return false
}

return self.content.isEquivalent(to: other.content)
}

public func newPresentationDecorationState(
kind : SupplementaryKind,
performsContentCallbacks : Bool
) -> Any
{
return PresentationState.DecorationState(
self,
kind: kind,
performsContentCallbacks: performsContentCallbacks
)
}
}


extension DecorationContent {

/// Identical to `Decoration.init` which takes in a `DecorationContent`,
/// except you can call this on the `DecorationContent` itself, instead of wrapping it,
/// to avoid additional nesting, and to hoist your content up in your code.
///
/// ```
/// Section("id") { section in
/// section.decoration = MyDecorationContent(
/// backgroundColor: .red
/// )
/// .with(
/// sizing: .thatFits(.noConstraint),
/// )
///
/// struct MyDecorationContent : DecorationContent {
/// var backgroundColor : UIColor
/// ...
/// }
/// ```
public func with(
sizing : Sizing? = nil,
layouts : DecorationLayouts? = nil,
onTap : Decoration<Self>.OnTap? = nil
) -> Decoration<Self>
{
Decoration(
self,
sizing: sizing,
layouts: layouts,
onTap: onTap
)
}
}


extension Decoration : SignpostLoggable
{
var signpostInfo : SignpostLoggingInfo {
SignpostLoggingInfo(
identifier: self.debuggingIdentifier,
instanceIdentifier: nil
)
}
}
33 changes: 33 additions & 0 deletions ListableUI/Sources/Decoration/DecorationCallbacks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// HeaderFooterCallbacks.swift
// ListableUI
//
// Created by Kyle Van Essen on 4/3/25.
//

import Foundation


extension Decoration {

/// Value passed to the `onDisplay` callback for `HeaderFooter`.
public struct OnDisplay
{
public typealias Callback = (OnDisplay) -> ()

public var decoration : Decoration

public var isFirstDisplay : Bool
}

/// Value passed to the `onEndDisplay` callback for `HeaderFooter`.
public struct OnEndDisplay
{
public typealias Callback = (OnEndDisplay) -> ()

public var decoration : Decoration

public var isFirstEndDisplay : Bool
}
}

Loading
Loading