FUISideBar

@available(iOS 14, *)
@MainActor
open class FUISideBar<Data>: UIView where Data: RandomAccessCollection,
                                          Data.Element: Identifiable & Hashable

A FUISideBar<Data> is a generic view that displaying data in an expandable list within a side bar.

Usage

Initialization:

Implement a data model for the expandable list that will be displayed in side bar.

struct BarItem: Identifiable, Hashable {
    let id = UUID()
    let title: String
    var icon: UIImage?
    var filledIcon: UIImage?
    var subtitle: String?
    var status: UIImage?
    let children: [BarItem]?

    func hash(into hasher: inout Hasher) {
        hasher.combine(self.id)
        hasher.combine(self.title)
    }

    static func == (lhs: BarItem, rhs: BarItem) -> Bool {
        lhs.id == rhs.id
    }

    init(title: String, icon: UIImage? = nil, filledIcon: UIImage? = nil, subtitle: String? = nil, status: UIImage? = nil, children: [BarItem]? = nil) {
        self.title = title
        self.icon = icon
        self.filledIcon = filledIcon
        self.subtitle = subtitle
        self.status = status
        self.children = children
    }
}

let items: [BarItem] = [
    BarItem(title: "Root Item 0.1", icon: UIImage(systemName: "cloud.snow"), subtitle: "9,999+", status: UIImage(systemName: "clock"), children: nil),
    BarItem(title: "Root Item 0.4", icon: UIImage(systemName: "cloud.snow"), children: nil),
    BarItem(title: "Group 1", children: [
        BarItem(title: "Child Item 1.1", icon: UIImage(systemName: "square.and.pencil"), subtitle: "66", status: UIImage(systemName: "circle"), children: nil),
        BarItem(title: "Child Item 1.2", icon: UIImage(systemName: "square.and.pencil"), status: UIImage(systemName: "circle"), children: nil)
    ]),
    BarItem(title: "Group 2", children: [
        BarItem(title: "Child Item 2.1", icon: UIImage(systemName: "folder"), subtitle: "5", status: UIImage(systemName: "mail"), children: nil)
    ])
]

Initialize an FUISideBar with the data model, a binding of row content view, and the initially selected item. Please note, with this initializer, the sidebar does not support editing.

let sidebar = FUISideBar(data: items,
                         children: \.children,
                         rowContent: { item in
                           FUISideBarListItem(icon: item.icon, filledIcon: item.filledIcon, title: item.title, subtitle: item.subtitle, accessoryIcon: item.status)
                         },
                         selectedItem: items.first)

Or, Initialize an FUISideBar with the data model, you will need to provide an array of FUISideBarItemModel, a binding of row content view, and the initially selected item. With this initializer, the sidebar support editing.

struct BarItem: FUISideBarItemModel {
    var id: UUID = UUID()
    var title: String
    var icon: UIImage?
    var filledIcon: UIImage?
    var subtitle: String?
    var status: UIImage?
    var isInvisible: Bool
    var children: [any FUISideBarItemModel]?

    static func == (lhs: EditableBarItem, rhs: EditableBarItem) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(self.id)
        hasher.combine(self.title)
    }

    init(title: String, icon: UIImage? = nil, filledIcon: UIImage? = nil, subtitle: String? = nil, status: UIImage? = nil, children: [EditableBarItem]? = nil, isInvisible: Bool = false) {
        self.title = title
        self.icon = icon
        self.filledIcon = filledIcon
        self.subtitle = subtitle
        self.status = status
        self.children = children
        self.isInvisible = isInvisible
    }
}

let items: [BarItem] = [
    BarItem(title: "Root Item 0.1", icon: UIImage(systemName: "square.dashed), filledIcon: UIImage(systemName: "square.dashed.inset.filled"), subtitle: "9,999+", status: UIImage(systemName: "clock"), children: nil),
    BarItem(title: "Root Item 0.4", icon: UIImage(systemName: "cloud.snow"), children: nil),
    BarItem(title: "Group 1", children: [
        BarItem(title: "Child Item 1.1", icon: UIImage(systemName: "square.and.pencil"), subtitle: "66", status: UIImage(systemName: "circle"), children: nil),
        BarItem(title: "Child Item 1.2", icon: UIImage(systemName: "square.and.pencil"), status: UIImage(systemName: "circle"), children: nil)
    ]),
    BarItem(title: "Group 2", children: [
        BarItem(title: "Child Item 2.1", icon: UIImage(systemName: "folder"), subtitle: "5", status: UIImage(systemName: "mail"), children: nil)
    ])
]

let sidebar = FUISideBar(data: items,
                         rowContent: { item in
                           FUISideBarListItem(icon: item.icon, filledIcon: item.filledIcon, title: item.title, subtitle: item.subtitle, accessoryIcon: item.status)
                         },
                         selectedItem: items.first)

Selection Handler:

A selectionDidChange closure may be used to custom the handling of an selected data element.

sidebar.selectionDidChange = { [weak self] item in
    self?.presentationDidChange?(item)
}

Handle Editing:

An ‘isEditing’ property of FUISideBar<Data> indicates whether the sidebar is in editing mode or not. Please note, it is essential to use the corresponding Initialize method which permits the editing of the sidebar. Additionally, an ‘Edit’ button can be added to the navigation bar to allow the sidebar to transition from editing mode to the view model.

self.navigationItem.rightBarButtonItem = self.editButtonItem

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)
    self.sideBar?.isEditing = editing
    configNavBarStyle()
    configureSearchBarOnNavBar(true)
}

Monitor changed data:

A dataChange closure may be used to receive notifications when the sidebar transitions from its editing mode to the view model if the sidebar item was reordered or visible was changed. This arrangement facilitates effective monitoring and management of changes. This should be used exclusively when the sidebar supports editing and you are particularly concerned about the modified data.

sideBar?.dataChange = { (items: [BarItem]) in
    // Check data changes
}

A ‘queryString’ property of FUISideBar<Data> is used to trigger the searching on SideBar. You can add the FUISearchController as navigation item for searching and get the query string from UISearchResultsUpdating.

let fuiSearchController = addResultsController ? FUISearchController(searchResultsController: SearchResultsViewController()) : FUISearchController(searchResultsController: nil)
fuiSearchController.searchResultsUpdater = self
fuiSearchController.hidesNavigationBarDuringPresentation = hidesNavBar
fuiSearchController.setsNavigationBarColor = true
fuiSearchController.setsStatusBarStyle = true
fuiSearchController.searchBar.placeholderText = "Search"

fuiSearchController.searchBar.delegate = self
self.searchController = fuiSearchController

navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = true // false

func updateSearchResults(for searchController: UISearchController) {
   self.sideBar?.queryString = searchController.searchBar.text
}
  • String for displaying subtitle in the side bar.

    Declaration

    Swift

    @MainActor
    public private(set) var subtitle: String? { get }
  • Footer view of the side bar.

    Declaration

    Swift

    @MainActor
    public private(set) var footer: UIView? { get }
  • Bool to indicate whether the side bar is in editing mode or not

    Declaration

    Swift

    @MainActor
    public var isEditing: Bool { get set }
  • String for searching in the side bar

    Declaration

    Swift

    @MainActor
    public var queryString: String? { get set }
  • Closure that defining behaviors once selection has been changed.

    Declaration

    Swift

    @MainActor
    public var selectionDidChange: ((Data.Element?) -> Void)? { get set }
  • Closure that defines behaviors once the sidebar transitions from editing mode to the view mode, and the data of the sidebar has been changed, such as reordering, visibility changes, etc. The data passed into the closure includes all the changed data. This facilitates effective monitoring and management of changes.

    Declaration

    Swift

    @MainActor
    public var dataChange: (([any FUISideBarItemModel]) -> Void)?
  • Public initializer for FUISideBar. with this initializer, the sidebar does not support editing

    Declaration

    Swift

    @MainActor
    public init(subtitle: String? = nil,
                footer: UIView? = nil,
                data: Data,
                children: KeyPath<Data.Element, Data?>,
                rowContent: @escaping (Data.Element) -> UIView,
                selectedItem: Data.Element? = nil,
                selectionDidChange: ((Data.Element?) -> Void)? = nil)

    Parameters

    subtitle

    Subtitle for the side bar.

    footer

    Footer view for the side bar.

    data

    The data for constructing the expandable list within the side bar.

    children

    The key path to the optional property of a data element whose value indicates the children of that element.

    rowContent

    The data-binding closure which returns the view of each row in an expandable list.

    selectedItem

    Initially selected data element.

    selectionDidChange

    The closure that defines behaviors once a data element in the expandable list has been selected.

  • Public initializer for FUISideBar. With this initializer, the sidebar supports editing by setting the ‘isEditing’ property of sidebar

    Declaration

    Swift

    @MainActor
    public init(footer: UIView? = nil,
                data: [any FUISideBarItemModel],
                rowContent: @escaping (any FUISideBarItemModel) -> UIView,
                selectedItem: (any FUISideBarItemModel)? = nil,
                selectionDidChange: (((any FUISideBarItemModel)?) -> Void)? = nil)

    Parameters

    footer

    Footer view for the side bar.

    data

    The array of ‘FUISideBarItemModel’ for constructing the expandable list within the side bar which supports editing.

    rowContent

    The data-bound closure which returns the view of each row in an expandable list.

    selectedItem

    Initially selected data element.

    selectionDidChange

    The closure that defines behaviors once a data element in the expandable list has been selected.