How can you model a tree structure using NSDiffableDataSourceSectionSnapshot? I would like to make something that resembles a family tree. SwiftUI makes this relatively simple by using the built in List.
First we have to make our node object to represent heretical tree data.
import Foundation
#resultBuilder
struct NodeBuilder {
static func buildBlock<Value>(_ children: Node<Value>...) -> [Node<Value>] {
children
}
}
struct Node<Value>: Identifiable {
var value: Value
private(set) var children: [Node]?
var id = UUID()
mutating func add(child: Node) {
children?.append(child)
}
init(_ value: Value) {
self.value = value
}
init(_ value: Value, children: [Node]) {
self.value = value
}
init(_ value: Value, #NodeBuilder builder: () -> [Node]) {
self.value = value
self.children = builder()
}
var count: Int {
1 + (children?.reduce(0) { $0 + $1.count } ?? 0)
}
var recursiveChildren: [Node] {
return [self] + (children?.flatMap { $0.recursiveChildren } ?? [])
}
}
extension Node: Equatable where Value: Equatable { }
extension Node: Hashable where Value: Hashable { }
extension Node: Codable where Value: Codable { }
Next, and most importantly we configure the NSDiffableDataSourceSectionSnapshot using recursion. See applySnapshot and addChildren.
class ViewController: UIViewController {
private var collectionView: UICollectionView! = nil
var dataSource: UICollectionViewDiffableDataSource<String, Node<String>>! = nil
let root = Node("Terry") {
Node("Paul") {
Node("Sophie")
Node("Timmy")
Node("Sandra") {
Node("Aimee")
Node("Niki")
}
Node("Bob")
}
Node("Andrew") {
Node("John")
Node("Adam")
Node("Suzzie")
Node("Ricky"){
Node("Taylor")
Node("Megan")
Node("Arthur") {
Node("Fred")
Node("George")
Node("Giny") {
Node("Harry")
Node("Harold")
}
}
}
}
}
//MARK: - View
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
configureDataSource()
applySnapshot(animated: false)
}
private func configureCollectionView() {
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
//MARK: - Data Source
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Node<String>> { (cell, indexPath, node) in
var content = cell.defaultContentConfiguration()
content.text = node.value
cell.contentConfiguration = content
cell.accessories = node.children == nil ? [] : [.outlineDisclosure()]
}
dataSource = UICollectionViewDiffableDataSource<String, Node<String>>(collectionView: collectionView) { (collectionView, indexPath, node) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: node)
}
}
func applySnapshot(animated: Bool) {
guard let children = root.children else { return }
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Node<String>>()
sectionSnapshot.append([root])
sectionSnapshot.append(children, to: root)
addChildren(of: root, to: §ionSnapshot)
sectionSnapshot.expand(sectionSnapshot.items)
dataSource.apply(sectionSnapshot, to: root.value, animatingDifferences: animated)
}
func addChildren(of node: Node<String>, to sectionSnapshot: inout NSDiffableDataSourceSectionSnapshot<Node<String>>) {
guard let children = node.children else { return }
for subChild in children {
if let grandChildren = subChild.children {
sectionSnapshot.append(grandChildren, to: subChild)
addChildren(of: subChild, to: §ionSnapshot)
}
}
}
}
If you would like multiple sections, that's easily done as well by modifying the applySnapshot function.
func applySnapshot(animated: Bool) {
guard let children = root.children else { return }
for child in children {
if let grandChildren = child.children {
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Node<String>>()
sectionSnapshot.append([child])
sectionSnapshot.append(grandChildren, to: child)
addChildren(of: child, to: §ionSnapshot)
sectionSnapshot.expand(sectionSnapshot.items)
dataSource.apply(sectionSnapshot, to: child.value, animatingDifferences: animated)
}
}
}
Related
In my swift code below right now the code saves 2 boolean values to a core data boolean value. The two values are true false. I would like the user to turn on the switch and have the 2nd value be true as well. So it would be true true. I am trying to do that in func alternate() but I dont know how to exactly effect a specific value. Looking for any kind of help.
import UIKit;import CoreData
class ViewController: UIViewController {
var lbl = UILabel()
var sw = UISwitch()
var checkmarkButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[ lbl,sw,checkmarkButton].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.layer.borderWidth = 1
}
helpBool.shareInstance.saveBoolean(true)
helpBool.shareInstance.saveBoolean(false)
getBool(imageNo: 1)
}
func getBool(imageNo:Int) {
// first check the array bounds
let info = helpBool.shareInstance.fetchBool()
if info.count > imageNo {
// if info[imageNo].bool {
if info[imageNo].bool == true {
checkmarkButton.setImage(UIImage(named:"unnamed"), for: .normal);
}
if info[imageNo].bool == false {
checkmarkButton.setImage(nil, for: .normal);
}
// }
}
}
#objc func alternate(){
//fetch alternate
getBool(imageNo: 1)
if sw.isOn = true {
helpBool.shareInstance.saveBoolean()
}
else {
helpBool.shareInstance.saveBoolean()
}
}
}
class helpBool: UIViewController{
static let shareInstance = helpBool()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveBoolean(_ boolean: Bool) {
let imageInstance = OnOff(context: context)
imageInstance.bool = boolean
do {
try context.save()
print("text is saved")
} catch {
print(error.localizedDescription)
}
}
func fetchBool() -> [OnOff] {
var fetchingImage = [OnOff]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "OnOff")
do {
fetchingImage = try context.fetch(fetchRequest) as! [OnOff]
} catch {
print("Error while fetching the image")
}
return fetchingImage
}
}
I have some code which loads in some empty sections and items. Then if there is a network connection some calls are made for the visible cells. I have a button in each section which navigates to another view based on the section type, which uses the indexPath. My problem is that when I remove the fist section the cellProvider doesn't update the indexPath and button navigates to the wrong view. If I scroll however the problem goes away. How do I make sure the indexPath updates? EDIT: It appears that my headerViews are retaining their old indexPath, but the cellProvider correctly updates. The header views are set to the correct indexPath once you scroll away and come back.
private enum RowItemType: Hashable {
case itemTypeOne(collection: MyCollection)
case itemTypeTwo(collection: MyCollection)
case itemTypeThree(collection: OtherCollection)
}
private enum SectionType: Hashable {
case sectionOne
case sectionTwo
case sectionThree
}
class MyCollection: Codable, Hashable, Equatable {
var identifier = UUID()
var foos: [Foo]
init(foos: [Foo] = [Foo]()) {
self.foos = foos
}
// MARK: Equatable
var hash: Int {
var hasher = Hasher()
hasher.combine(identifier)
return hasher.finalize()
}
static func == (lhs: MyCollection, rhs: MyCollection) -> Bool {
lhs.identifier == rhs.identifier
}
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
class OtherCollection: Codable, Hashable, Equatable {
var identifier = UUID()
var bars: [Bar]
init(bars: [Bar] = [Bar]()) {
self.bars = bars
}
// MARK: Equatable
var hash: Int {
var hasher = Hasher()
hasher.combine(identifier)
return hasher.finalize()
}
static func == (lhs: OtherCollection, rhs: OtherCollection) -> Bool {
lhs.identifier == rhs.identifier
}
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
final class SomeTableViewController: UITableVIewController {
...code....
RELEVENT CODE
private func makeDataSource() -> UITableViewDiffableDataSource<SectionType, RowItemType> {
UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, item in
guard let self = self else {
return UITableViewCell()
}
guard let marginTableView = tableView as? AutoMarginTableView else {
return UITableViewCell()
}
switch item {
case .itemTypeOne(collection: let collection):
let carousel = self.itemOneCarouCell(from: marginTableView, withSection: .sectionOne, atIndexPath: indexPath)
if !collection.foos.isEmpty {
carousel.configureCell(with: .sectionOne, fooCollection: collection)
self.showViewAllButtonForHeader(collection.foos.count > 3, atIndexPath: indexPath)
} else {
carousel.configureCellForLoading()
self.getFoosTypeOne(collection: collection)
}
return carousel
case .itemTypeTwo(let collection):
let carousel = self.itemTwoCarouCell(from: marginTableView, withSection: .sectionTwo, atIndexPath: indexPath)
if !collection.foos.isEmpty {
carousel.configureCell(with: .sectionTwo, fooCollection: collection)
self.showViewAllButtonForHeader(collection.foos.count > 3, atIndexPath: indexPath)
} else {
carousel.configureCellForLoading()
self.getFoosTypeTwo(collection: collection)
}
return carousel
case .ttemTypeThree(let collection):
let carousel = self.itemThreeCarouCell(from: marginTableView, withSection: .sectionThree, atIndexPath: indexPath)
if !collection.bars.isEmpty {
carousel.configureCell(with: collection)
self.showViewAllButtonForHeader(true, atIndexPath: indexPath)
} else {
carousel.configureCellForLoading()
self.getBars(collection: collection)
}
return carousel
})
}
private func showViewAllButtonForHeader(_ show: Bool, atIndexPath indexPath: IndexPath) {
if let headerView = self.tableView.headerView(forSection: indexPath.section) as? MyTableHeaderView {
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseInOut, animations: {
headerView.showViewAll(show)
}, completion: nil)
}
}
private func getFoosTypeOne(collection: MyCollection) {
var snapshot = dataSource.snapshot()
if FeatureFlags.isFeatureEnabled && snapshot.sectionIdentifiers.contains(.sectionOne) {
someRepositry.getfoos { result in
if case .success(let foos) = result {
if !foos.isEmpty {
collection.foos = foos
snapshot.reloadItems([.itemTypeOne(collection: collection)])
} else {
if snapshot.sectionIdentifiers.contains(.sectionOne) {
snapshot.deleteSections([.sectionOne])
}
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
private func getFoosTypeTwo(collection: MyCollection) {
var snapshot = dataSource.snapshot()
if FeatureFlags.isFeatureEnabled && snapshot.sectionIdentifiers.contains(.sectionTwo) {
someRepositry.getDifferentFoos { result in
if case .success(let foos) = result {
if !foos.isEmpty {
collection.foos = foos
snapshot.reloadItems([.itemTypeTwo(collection: collection)])
} else {
if snapshot.sectionIdentifiers.contains(.sectionTwo) {
snapshot.deleteSections([.sectionTwo])
}
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
private func getBars(collection: OtherCollection) {
var snapshot = dataSource.snapshot()
someService.getSomeCollection { someColl in
DispatchQueue.main.async {
if let someColl = someColl {
if !someColl.bars.isEmpty {
collection.bars = someColl.bars
snapshot.reloadItems([.itemTypeThree(collection: collection)])
} else {
if snapshot.sectionIdentifiers.contains(.sectionThree) {
snapshot.deleteSections([.sectionThree])
}
}
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
extension SomeTabelViewController: SomeTableHeaderViewDelegate {
func someTableHeaderViewTappedViewAll(_ headerView: SomeTableHeaderView) {
guard headerView.section < dataSource.numberOfSections(in: tableView) else {
return
}
let snapshot = dataSource.snapshot()
let section = snapshot.sectionIdentifiers[headerView.section]
switch section {
case .sectionOne:
if case .itemTypeOne(let collection) = snapshot.itemIdentifiers(inSection: .sectionOne).first, !collection.foos.isEmpty {
let vc = SomeListViewController()
navigationController?.pushViewController(vc, animated: true)
}
case .sectionTwo:
if case .itemTypeTwo(let collection) = snapshot.itemIdentifiers(inSection: .sectionTwo).first, !collection.foos.isEmpty {
let vc = SomeListViewController()
navigationController?.pushViewController(vc, animated: true)
}
case .sectionThree:
if case .itemTypeThree(let collection) = snapshot.itemIdentifiers(inSection: .sectionThree).first, !collection.bars.isEmpty {
let vc = OtherViewController(collecton: collection)
navigationController?.pushViewController(vc, animated: true)
}
}
}
RxDatasource in RxSwift [RxTableViewSectionedAnimatedDataSource] Reload Animation don't update data source. What mistake I am doing? I am even unable to bind my action with button properly.
TableDataSource and Table editing commands
struct TableDataSource {
var header: String
var items: [Item]
var SectionViewModel: SectionViewModel
}
extension TableDataSource: AnimatableSectionModelType {
var identity: String {
return header
}
type alias Item = Data
init(original: TableDataSource, items: [Item]) {
self = original
self.items = items
self.sectionViewModel = original.sectionViewModel
}
}
enum TableViewEditingCommand {
case deleteSingle(IndexPath)
case clearAll(IndexPath)
case readAll(IndexPath)
}
struct SectionedTableViewState {
var sections: [TableDataSource]
init(sections: [TableDataSource]) {
self.sections = sections
}
func execute(command: TableViewEditingCommand) -> SectionedTableViewState {
var sections = self.sections
switch command {
// Delete single item from datasource
case .deleteSingle(let indexPath):
var items = sections[indexPath.section].items
items.remove(at: indexPath.row)
if items.count <= 0 {
sections.remove(at: indexPath.section)
} else {
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: items) }
// Clear all item from datasource with isUnread = false
case .clearAll(let indexPath):
sections.remove(at: indexPath.section)
// Mark all item as read in datasource with isUnread = true
case .readAll(let indexPath):
var items = sections[indexPath.section].items
items = items.map { var unread = $0
if $0.isUnRead == true { unreadData.isUnRead = false }
return unreadData
}
sections.remove(at: indexPath.section)
if sections.count > 0 {
let allReadItems = sections[indexPath.section].items + items
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: allReadItems)
}
}
return SectionedTableViewState(sections: sections)
}
}
This is my controller and its extensions
class ViewController: UIViewController, Storyboardable {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var closeButton: UIButton!
#IBOutlet weak var titleText: UILabel!
var viewModel: ViewModel!
let disposeBag = DisposeBag()
let sectionHeight: CGFloat = 70
let dataSource = ViewController.dataSource()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
bindInitials()
bindDataSource()
bindDelete()
}
private func bindInitials() {
tableView.delegate = nil
tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
registerNibs()
}
private func registerNibs() {
let headerNib = UINib.init(
nibName: TableViewSection.identifier,
bundle: nil)
tableView.register(
headerNib,
forHeaderFooterViewReuseIdentifier: TableViewSection.identifier)
}
}
extension ViewController: Bindable {
func bindViewModel() {
bindActions()
}
private func bindDataSource() {
bindDelete()
// tableView.dataSource = nil
// Observable.just(sections)
// .bind(to: tableView.rx.items(dataSource: dataSource))
// .disposed(by: disposeBag)
}
private func bindDelete() {
/// TODO: to check and update delete code to work properly to sink with clear all and mark all as read
guard let sections = self.viewModel?.getSections() else {
return
}
let deleteState = SectionedTableViewState(sections: sections)
let deleteCommand = tableView.rx.itemDeleted.asObservable()
.map(TableViewEditingCommand.deleteSingle)
tableView.dataSource = nil
Observable.of(deleteCommand)
.merge()
.scan(deleteState) {
(state: SectionedTableViewState,
command: TableViewEditingCommand) -> SectionedTableViewState in
return state.execute(command: command) }
.startWith(deleteState) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
private func bindActions() {
guard let openDetailsObserver = self.viewModel?.input.openDetailsObserver,
let closeObserver = self.viewModel?.input.closeObserver else {
return
}
viewModel.output.titleTextDriver
.drive(titleText.rx.text)
.disposed(by: disposeBag)
// viewModel.input.dataSourceObserver
// .mapObserver({ (result) -> [Data] in
// return result
// })
// .disposed(by: disposeBag)
/// Close button binding with closeObserver
closeButton.rx.tap
.bind(to: (closeObserver))
.disposed(by: disposeBag)
/// Tableview item selected binding with openDetailsObserver
tableView.rx.itemSelected
.map { indexPath in
return (self.dataSource[indexPath.section].items[indexPath.row])
}.subscribe(openDetailsObserver).disposed(by: disposeBag)
}
}
extension ViewController: UITableViewDelegate {
static func dataSource() -> RxTableViewSectionedAnimatedDataSource<TableDataSource> {
return RxTableViewSectionedAnimatedDataSource(
animationConfiguration: AnimationConfiguration(insertAnimation: .fade,
reloadAnimation: .fade,
deleteAnimation: .fade),
configureCell: { (dataSource, table, idxPath, item) in
var cell = table.dequeueReusableCell(withIdentifier: TableViewCell.identifier) as? TableViewCell
let cellViewModel = TableCellViewModel(withItem: item)
cell?.setViewModel(to: cellViewModel)
return cell ?? UITableViewCell()
}, canEditRowAtIndexPath: { _, _ in return true })
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard var headerView = tableView.dequeueReusableHeaderFooterView(
withIdentifier: TableViewSection.identifier)
as? TableViewSection
else { return UITableViewHeaderFooterView() }
let viewModel = self.dataSource[section].sectionViewModel
headerView.setViewModel(to: viewModel)
headerView.dividerLine.isHidden = section == 0 ? true : false
headerView.section = section
let data = TableViewEditingCommand.clearAll(IndexPath(row: 0, section: section ?? 0))
// /// Section button binding with closeObserver
// headerView.sectionButton.rx.tap
// .map(verseNum -> TableViewEditingCommand in (TableViewEditingCommand.deleteSingle))
// .disposed(by: disposeBag)
headerView.sectionButtonTappedClosure = { [weak self] (section, buttonType) in
if buttonType == ButtonType.clearAll {
self?.showClearAllConfirmationAlert(section: section, buttonType: buttonType)
} else {
self?.editAction(section: section, buttonType: buttonType)
}
}
return headerView
}
func editAction(section: Int, buttonType: ButtonType) {
var sections = self.dataSource.sectionModels
let updateSection = (sections.count == 1 ? 0 : section)
switch buttonType {
/// Clear all
case .clearAll:
sections.remove(at: updateSection)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
/// Mark all as read
case .markAllAsRead:
if updateSection == 0 { sections = self.viewModel.getSections() }
var items = sections[updateSection].items
items = items.map { var unread = $0
if $0.isUnRead == true { unread.isUnRead = false }
return unread
}
sections.remove(at: updateSection)
let allReadItems = sections[updateSection].items + items
sections[updateSection] = TableDataSource(
original: sections[updateSection],
items: allReadItems)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
func showClearAllConfirmationAlert(section: Int, buttonType: ButtonType) {
let alert = UIAlertController(title: "Clear All",
message: "Are you sure, you want to clear all Items?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
switch action.style{
case .default:
self.editAction(section: section, buttonType: buttonType)
case .cancel: break
case .destructive: break
default:break
}}))
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
})
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sectionHeight
}
}
View model for controller
class ViewModel {
private enum Constants {
static let titleText = "Test".localized
static let testHistoryHeaderText = "test".localized
static let unreadHeaderText = "Unread".localized
}
struct Input {
let dataSourceObserver: AnyObserver<[Data]>
let openDetailsObserver: AnyObserver<Data>
let closeObserver: AnyObserver<Void>
let sectionButtonTappedObserver: AnyObserver<IndexPath>
}
struct Output {
let titleTextDriver: Driver<String>
let dataSourceDriver: Driver<[Data]>
let viewComplete: Observable<DataCoordinator.Event>
}
let input: Input
let output: Output
private let dataSourceSubject = PublishSubject<[Data]>()
private let closeSubject = PublishSubject<Void>()
private let openDetailsSubject = BehaviorSubject<Data>(value:Data())
private let sectionButtonTappedSubject = PublishSubject<IndexPath>()
private let disposeBag = DisposeBag()
init(withRepository repository: Repository) {
input = Input(
dataSourceObserver: dataSourceSubject.asObserver(),
openDetailsObserver: openDetailsSubject.asObserver(),
closeObserver: closeSubject.asObserver(), sectionButtonTappedObserver: sectionButtonTappedSubject.asObserver()
)
let closeEventObservable = closeSubject.asObservable().map { _ in
return Coordinator.Event.goBack
}
let openDetailsEventObservable = openDetailsSubject.asObservable().map { _ in
return Coordinator.Event.goToDetails
}
let viewCompleteObservable = Observable.merge(closeEventObservable, openDetailsEventObservable)
let list = ViewModel.getData(repository: repository)
output = Output(
titleTextDriver: Observable.just(Constants.titleText).asDriver(onErrorJustReturn: Constants.titleText),
dataSourceDriver: Observable.just(list).asDriver(onErrorJustReturn: list),
viewComplete: viewCompleteObservable)
}
///TODO: To be updated as per response after API integration
static func getData(repository: Repository) -> [Data] {
return repository.getData()
}
func getSections() -> [TableDataSource] {
let List = ViewModel.getData(repository: Repository())
let unread = list.filter { $0.isUnRead == true }
let read = list.filter { $0.isUnRead == false }
let headerText = String(format:Constants.unreadHeaderText, unread.count)
let sections = [TableDataSource(
header: headerText,
items: unread,
sectionViewModel: SectionViewModel(
withHeader: headerText,
buttonType: ButtonType.markAllAsRead.rawValue,
items: unread)),
TableDataSource(
header: Constants.historyHeaderText,
items: read,
SectionViewModel: SectionViewModel(
withHeader: Constants.historyHeaderText,
buttonType: ButtonType.clearAll.rawValue,
items: read))]
return sections
}
}
i think your problem is with :
var identity: String {
return header
}
make a uuid and pass it to identity:
let id = UUID().uuidString
var identity: String {
return id
}
Im trying to expand and select a child of an item on a outline view, but the only thing I can do is select the root item and expand only this item and not its children.
I populate an outlineview with directories. Every time an item is expandable, its sub directories are loaded.
I had some help (How to expand from a child to its very first parent?) that makes the root selected and expanded.
When expanding an item, I save the location. So, next time the app is opened it will show all item expanded from root to the saved path.
For example, when I select Desktop, close and open the app, it shows Desktop selected and expanded.
If I select dial or mp under Desktop, I can save the path but when open, it goes back to the root and nothing is selected or expanded.
End result would be with dial or mp selected and expanded.
the function expandItem has a parameter expandChild, but it expands everything. I only want to expand until the saved path.
var rootItem = DirectoryItem(url: FileManager.default.homeDirectoryForCurrentUser)
override func viewDidLoad() {
super.viewDidLoad()
let defaults = UserDefaults.standard
let lastDirectory = defaults.url(forKey: "LastDirectory")
outlineView.dataSource = self
outlineView.delegate = self
tableView.delegate = self
tableView.dataSource = self
tableView.doubleAction = #selector(doubleClickOnResultRow)
self.progressIndicator.isHidden = true
self.tableView.reloadData()
self.outlineView.reloadData()
reveal(URL(string: lastDirectory!.lastPathComponent) ?? FileManager.default.homeDirectoryForCurrentUser)
}
func reveal(_ url: URL) {
let items = itemHierarchy(for: url)
for item in items {
outlineView.expandItem(item, expandChildren: false)
let row = outlineView.row(forItem: item)
if(row != -1) {
let set = IndexSet(integer: row)
outlineView.selectRowIndexes(set, byExtendingSelection: false)
}
}
}
private func itemHierarchy(for url: URL) -> [DirectoryItem] {
var items: [DirectoryItem] = []
var current: DirectoryItem = rootItem
for component in url.pathComponents {
guard let child = current.childItems.first(where: { $0.name == component }) else {
return items
}
items.append(child)
current = child
}
return items
}
extension ViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
let directoryItem = item as? DirectoryItem ?? rootItem
return directoryItem.childItems.count
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
let directoryItem = item as? DirectoryItem ?? rootItem
return directoryItem.childItems[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
let directoryItem = item as? DirectoryItem ?? rootItem
return directoryItem.isExpandable
}
}
extension ViewController: NSOutlineViewDelegate {
func outlineViewSelectionDidChange(_ notification: Notification) {
singleClick()
}
func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool {
let directoryItem = item as? DirectoryItem ?? rootItem
return (directoryItem.childItems.count != 0)
}
func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool {
let directoryItem = item as? DirectoryItem ?? rootItem
return (directoryItem.childItems.count != 0)
}
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
var text = ""
if let directories = item as? DirectoryItem {
if(directories.isdir) {
text = directories.name
let tableCell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "cell"), owner: self) as! NSTableCellView
tableCell.textField!.stringValue = text
return tableCell
}
}
return nil
}
}
class Directories {
var name: String
var subDirectories: [String]
init(name: String, subDirectories: [String]) {
self.name = name
self.subDirectories = subDirectories
}
}
class DirectoryItem: CustomDebugStringConvertible {
var name: String
var url: URL
var isdir: Bool
var prev: URL
lazy var isExpandable: Bool = {
do {
return try url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
} catch let error as NSError {
return false
}
}()
lazy var childItems: [DirectoryItem] = {
do {
let urls = try FileManager.default.contentsOfDirectory(at: url,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles])
var aa: [DirectoryItem]
var bb: [DirectoryItem]
bb = []
aa = urls.map { DirectoryItem(url: $0) }
for a in aa {
if(a.isdir) {
bb.append(a)
// print(a)
}
}
return bb
//return urls.map { DirectoryItem(url: $0) }
} catch let error as NSError {
return []
}
}()
init(url: URL) {
self.url = url
self.name = url.lastPathComponent
self.isdir = url.hasDirectoryPath
self.prev = url.deletingLastPathComponent()
}
public var debugDescription: String {
return """
- url: \(self.url)
- name: \(self.name)
- isdir: \(self.isdir)
- prev: \(self.prev)
"""
}
}
Is there a simple way to do this for a UITabBarController after the user has modified the original ordering? I got the following code to work but it feels a bit clunky.
class TabBarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.restoreItemOrder()
}
override func tabBar(_ tabBar: UITabBar, didEndCustomizing items: [UITabBarItem], changed: Bool) {
if changed {
self.cacheItemOrder(items: items)
}
}
}
The extensions to cache/restore the order after editing.
extension UITabBarController {
private static let cacheKey = "tabBarIndex"
public func cacheItemOrder(items: [UITabBarItem]) {
var index = [String: Int]()
var i = 0
for item in items {
if let title = item.title {
index[title] = i
}
i += 1
}
UserDefaults.standard.set(index, forKey: UITabBarController.cacheKey)
}
public func restoreItemOrder() {
if let dict = UserDefaults.standard.dictionary(forKey: UITabBarController.cacheKey) as? [String: Int] {
if let vcs = self.viewControllers {
self.viewControllers = vcs.sorted() {
var idx0 = 99
var idx1 = 99
if let t0 = $0.tabBarItem.title, let idx = dict[t0] {
idx0 = idx
}
if let t1 = $1.tabBarItem.title, let idx = dict[t1] {
idx1 = idx
}
return idx0 < idx1
}
}
}
}
}
Thanks in advance.
You can express everything more succinctly with functional idioms:
extension UITabBarController {
private static let cacheKey = "tabBarIndex"
public func cacheItemOrder(items: [UITabBarItem]) {
UserDefaults.standard.set(
[String: Int](uniqueKeysWithValues: items.enumerated().map { ($1.title ?? "", $0)}),
forKey: UITabBarController.cacheKey
)
}
public func restoreItemOrder() {
guard let order = UserDefaults.standard.dictionary(forKey: UITabBarController.cacheKey) as? [String: Int] else {
return
}
viewControllers?.sort { left, right in
guard let leftIndex = left.title.flatMap({order[$0]}),
let rightIndex = right.title.flatMap({order[$0]}) else {
return false
}
return leftIndex < rightIndex
}
}
}