├── .gitignore ├── .swift-version ├── .travis.yml ├── LICENSE ├── LNZTreeView.gif ├── LNZTreeView.podspec ├── LNZTreeView ├── Info.plist ├── LNZTreeView.h ├── LNZTreeView.swift ├── LNZTreeViewDataSource.swift └── LNZTreeViewDelegate.swift ├── LNZTreeViewDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ └── LNZTreeView.xcscheme └── xcuserdata │ └── giuseppelanza.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LNZTreeViewDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── index_folder_indicator.imageset │ │ ├── Contents.json │ │ └── index_folder_indicator.pdf │ └── index_folder_indicator_open.imageset │ │ ├── Contents.json │ │ └── index_folder_indicator_open.pdf ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── LNZTreeViewTests ├── Info.plist ├── LNZTreeViewTests.swift └── MockObjects │ ├── TestNode.swift │ ├── TreeViewMockDataSource.swift │ └── TreeViewMockDelegate.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10 3 | 4 | script: 5 | - xcodebuild -project LNZTreeViewDemo.xcodeproj -scheme LNZTreeView -destination "OS=11.1,name=iPhone 7 Plus" -configuration Debug ONLY_ACTIVE_ARCH=NO test ENABLE_TESTABILITY=YES test | xcpretty; 6 | after_success: 7 | - bash <(curl -s https://codecov.io/bash) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Giuseppe Lanza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LNZTreeView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gringoireDM/LNZTreeView/4cf95c2dcdf7a4368c9a34dd270fdc3482397d2c/LNZTreeView.gif -------------------------------------------------------------------------------- /LNZTreeView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.platform = :ios 4 | s.version = "1.1.2" 5 | s.ios.deployment_target = '8.0' 6 | s.name = "LNZTreeView" 7 | s.summary = "A swift TreeView implementation for iOS." 8 | 9 | s.description = <<-DESC 10 | This is a swift implementation for iOS of a Tree View. A Tree View is a graphical representation of a tree. Each element (node) can have a number of sub elements (children). 11 | 12 | This particular implementation of TreeView organizes nodes and subnodes in rows and each node has an indentation that indicates the hierarchy of the element. A parent can be expanded or collapsed and each children can be a parent itself containing more sub nodes. 13 | DESC 14 | 15 | s.requires_arc = true 16 | 17 | s.license = { :type => "MIT" } 18 | s.homepage = "https://www.pfrpg.net" 19 | s.author = { "Giuseppe Lanza" => "gringoire986@gmail.com" } 20 | s.source = { 21 | :git => "https://github.com/gringoireDM/LNZTreeView.git", 22 | :tag => "v1.1.2" 23 | } 24 | 25 | s.framework = "UIKit" 26 | 27 | s.source_files = "LNZTreeView/**/*.{swift, h}" 28 | end -------------------------------------------------------------------------------- /LNZTreeView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.2 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LNZTreeView/LNZTreeView.h: -------------------------------------------------------------------------------- 1 | // 2 | // LNZTreeView.h 3 | // LNZTreeView 4 | // 5 | // Created by Giuseppe Lanza on 07/11/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for LNZTreeView. 12 | FOUNDATION_EXPORT double LNZTreeViewVersionNumber; 13 | 14 | //! Project version string for LNZTreeView. 15 | FOUNDATION_EXPORT const unsigned char LNZTreeViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /LNZTreeView/LNZTreeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LNZTreeView.swift 3 | // PFRPG rd 4 | // 5 | // Created by Giuseppe Lanza on 23/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol TreeNodeProtocol { 12 | var identifier: String { get } 13 | var isExpandable: Bool { get } 14 | } 15 | 16 | @IBDesignable @objcMembers 17 | public class LNZTreeView: UIView { 18 | class MinimalTreeNode { 19 | var identifier: String 20 | var indentationLevel: Int = 0 21 | 22 | var isExpandable: Bool = false 23 | var isExpanded: Bool = false 24 | 25 | 26 | var parent: TreeNodeProtocol? 27 | 28 | init(identifier: String) { 29 | self.identifier = identifier 30 | } 31 | } 32 | 33 | 34 | 35 | @IBInspectable public var indentationWidth: CGFloat = 10 36 | @IBInspectable public var isEditing: Bool { 37 | get { return tableView.isEditing } 38 | set { tableView.isEditing = newValue } 39 | } 40 | 41 | @IBInspectable public var rowHeight: CGFloat { 42 | get { return tableView.rowHeight } 43 | set { tableView.rowHeight = newValue } 44 | } 45 | 46 | @IBInspectable public var allowsSelectionDuringEditing: Bool { 47 | get { return tableView.allowsSelectionDuringEditing } 48 | set { tableView.allowsSelectionDuringEditing = newValue } 49 | } 50 | 51 | public func setEditing(_ editing: Bool, animated: Bool) { 52 | tableView.setEditing(editing, animated: animated) 53 | } 54 | 55 | lazy var tableView: UITableView! = { 56 | return UITableView(frame: frame, style: .plain) 57 | }() 58 | 59 | public var keyboardDismissMode : UIScrollView.KeyboardDismissMode { 60 | get { 61 | return tableView.keyboardDismissMode 62 | } 63 | set{ 64 | tableView.keyboardDismissMode = newValue 65 | } 66 | } 67 | 68 | public var tableViewRowAnimation: UITableView.RowAnimation = .right 69 | 70 | var nodesForSection = [Int: [MinimalTreeNode]]() 71 | 72 | @IBOutlet public weak var dataSource: LNZTreeViewDataSource? 73 | @IBOutlet public weak var delegate: LNZTreeViewDelegate? 74 | 75 | public override init(frame: CGRect) { 76 | super.init(frame: frame) 77 | commonInit() 78 | } 79 | 80 | public required init?(coder aDecoder: NSCoder) { 81 | super.init(coder: aDecoder) 82 | commonInit() 83 | } 84 | 85 | public convenience init() { 86 | self.init(frame: .zero) 87 | } 88 | 89 | private func commonInit() { 90 | tableView.delegate = self 91 | tableView.dataSource = self 92 | tableView.translatesAutoresizingMaskIntoConstraints = false 93 | addSubview(tableView) 94 | 95 | if #available(iOS 11.0, *) { 96 | let margins = safeAreaLayoutGuide 97 | tableView.leftAnchor.constraint(equalTo: margins.leftAnchor).isActive = true 98 | tableView.rightAnchor.constraint(equalTo: margins.rightAnchor).isActive = true 99 | tableView.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true 100 | tableView.bottomAnchor.constraint(equalTo: margins.bottomAnchor).isActive = true 101 | } else { 102 | addConstraints([ 103 | NSLayoutConstraint(item: tableView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0), 104 | NSLayoutConstraint(item: tableView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0), 105 | NSLayoutConstraint(item: tableView, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0), 106 | NSLayoutConstraint(item: tableView, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0) 107 | ]) 108 | } 109 | } 110 | 111 | 112 | open override func didMoveToSuperview() { 113 | super.didMoveToSuperview() 114 | 115 | tableView.reloadData() 116 | } 117 | 118 | /** 119 | All the tree hierarchy will be lost and the dataSource will be reloaded from scratch. 120 | */ 121 | public func resetTree() { 122 | nodesForSection.removeAll() 123 | 124 | tableView.reloadData() 125 | } 126 | 127 | /** 128 | This method returns the number of sections for the current load in dataSource. 129 | - returns: The number of sections. 130 | */ 131 | public func numberOfSections() -> Int { 132 | return tableView.numberOfSections 133 | } 134 | 135 | /** 136 | The number of total rows in a given section. This method returns the complete number regardless of the 137 | elements level in the tree. 138 | - parameter section: The index of the section of which you are requesting the number of rows. 139 | - returns: The number of nodes in the section. 140 | */ 141 | public func numberOfTotalNodesInSection(_ section: Int) -> Int { 142 | return nodesForSection[section]?.count ?? 0 143 | } 144 | 145 | /** 146 | The number of nodes in a given section, for nodes having as parent a given node. This method returns just 147 | the number of children, and just if the node is expanded, otherwise it will return 0. 148 | - parameter section: The index of the section of which you are requesting the number of rows. 149 | - parameter parent: The parent node you want the children count displayed in treeView. 150 | */ 151 | public func numberOfNodesForSection(_ section: Int, inParent parent: TreeNodeProtocol?) -> Int { 152 | return nodesForSection[section]?.filter( { parent?.identifier == $0.parent?.identifier }).count ?? 0 153 | } 154 | 155 | private func toggleExpanded(_ toggle: Bool, node: TreeNodeProtocol, inSection section: Int) -> Bool { 156 | guard node.isExpandable, 157 | let nodes = nodesForSection[section], 158 | let indexPath = indexPathForNode(node, inSection: section) else { 159 | return false 160 | } 161 | 162 | let minimalNode = nodes[indexPath.row] 163 | guard minimalNode.isExpanded != toggle else { return true } 164 | tableView(tableView, didSelectRowAt: indexPath) 165 | return minimalNode.isExpanded == toggle 166 | } 167 | 168 | /** 169 | Programmatically expand an expandable node. The return of this method indicates if the node was expanded. 170 | - parameter node: The node to be expanded. 171 | - parameter section: The index of the section where the node is. 172 | - returns: true if the node was successfully expanded, false otherwise. 173 | */ 174 | @discardableResult 175 | public func expand(node: TreeNodeProtocol, inSection section: Int) -> Bool { 176 | return toggleExpanded(true, node: node, inSection: section) 177 | } 178 | 179 | /** 180 | Programmatically collapse an expanded expandable node. The return of this method indicates if the node 181 | was collapsed. 182 | - parameter node: The node to be collapsed. 183 | - parameter section: The section index where the node is. 184 | - returns: true if the node was successfully collapsed, false otherwise. 185 | */ 186 | @discardableResult 187 | public func collapse(node: TreeNodeProtocol, inSection section: Int) -> Bool { 188 | return toggleExpanded(false, node: node, inSection: section) 189 | } 190 | 191 | /** 192 | Programmatically select a node. If the node is expandable, the expand toggle will be triggered, which means 193 | that if it is expanded it will be collapsed, viceversa if it is collapsed it will be expanded. 194 | - parameter node: The node to be selected. 195 | - parameter section: The section index where the node is. 196 | - parameter animated: An animation will occur on select. 197 | - parameter scrollPosition: the scroll position for the selected node. 198 | - returns: true if the node was successfully selected. False otherwise. 199 | */ 200 | @discardableResult 201 | public func select(node: TreeNodeProtocol, inSection section: Int, animated: Bool = false, scrollPosition: UITableView.ScrollPosition = .none) -> Bool { 202 | guard let indexPath = indexPathForNode(node, inSection: section) else { return false } 203 | tableView.selectRow(at: indexPath, animated: animated, scrollPosition: scrollPosition) 204 | tableView(tableView, didSelectRowAt: indexPath) 205 | return true 206 | } 207 | 208 | /** 209 | Retrieve the index path for a given node in a given section. 210 | */ 211 | private func indexPathForNode(_ node: TreeNodeProtocol, inSection section: Int) -> IndexPath? { 212 | return indexPathForNode(withIdentifier: node.identifier, inSection: section) 213 | } 214 | 215 | private func indexPathForNode(withIdentifier identifier: String, inSection section: Int) -> IndexPath? { 216 | guard let nodes = nodesForSection[section], 217 | let nodeIndex = nodes.index(where: { $0.identifier == identifier }) else { 218 | return nil 219 | } 220 | return IndexPath(row: nodeIndex, section: section) 221 | } 222 | 223 | //MARK: Cells Reusability 224 | 225 | /** 226 | Register a class for the cell to use to instanciate any new cell for a given identifier. 227 | 228 | - parameter cellClass: The class of a cell that you want to use in the table. 229 | - parameter identifier: The reuse identifier for the cell. This parameter must not be nil and must not be an empty string. 230 | */ 231 | @objc(registerCellClass:forCellReuseIdentifier:) 232 | public func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String) { 233 | tableView.register(cellClass, forCellReuseIdentifier: identifier) 234 | } 235 | 236 | /** 237 | Registers a nib object containing a cell with the table view under a specified identifier. 238 | 239 | - parameter nib: A nib object that specifies the nib file to use to create the cell. 240 | - parameter identifier: The reuse identifier for the cell. This parameter must not be nil and must not be an empty string. 241 | */ 242 | @objc(registerNib:forCellReuseIdentifier:) 243 | public func register(_ nib: UINib?, forCellReuseIdentifier identifier: String) { 244 | tableView.register(nib, forCellReuseIdentifier: identifier) 245 | } 246 | 247 | /** 248 | Returns a reusable table-view cell object for the specified reuse identifier and adds it to the table. 249 | ###Important 250 | You must register a class or nib file using the register(_:forCellReuseIdentifier:) or register(_:forCellReuseIdentifier:) method before calling this method. 251 | If you registered a class for the specified identifier and a new cell must be created, this method initializes the cell by calling its init(style:reuseIdentifier:) method. For nib-based cells, this method loads the cell object from the provided nib file. If an existing cell was available for reuse, this method calls the cell’s prepareForReuse() method instead. 252 | - parameter identifier: A string identifying the cell object to be reused. This parameter must not be nil. 253 | - parameter indexPath: The index path specifying the location of the cell. The data source receives this information when it is asked for the cell and should just pass it along. This method uses the index path to perform additional configuration based on the cell’s position in the table view. 254 | */ 255 | public func dequeueReusableCell(withIdentifier identifier: String, for node: TreeNodeProtocol, inSection section: Int) -> UITableViewCell { 256 | let indexPath = indexPathForNode(node, inSection: section)! 257 | return tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) 258 | } 259 | 260 | public func dequeueReusableCell(withIdentifier identifier: String) -> UITableViewCell? { 261 | return tableView.dequeueReusableCell(withIdentifier:identifier) 262 | } 263 | 264 | /** 265 | Set the scroll position to match the position of the current node in the TreeView. 266 | - parameter node: The node to scroll to. 267 | - parameter section: The section index where the node is. 268 | - parameter scrollPosition: The scroll position. 269 | - parameter animated: This parameter indicates if the scroll must be animated. 270 | */ 271 | public func scrollToNode(_ node: TreeNodeProtocol, inSection section: Int, scrollPosition: UITableView.ScrollPosition = .middle, animated: Bool = true) { 272 | guard let indexPath = indexPathForNode(node, inSection: section) else { return } 273 | tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: animated) 274 | } 275 | 276 | /** 277 | Return the current node for the current selected row. 278 | 279 | - returns: The current node for the selected row, or nil if no row was selected 280 | */ 281 | public func nodeForSelectedRow() -> TreeNodeProtocol? { 282 | guard let indexPath = tableView.indexPathForSelectedRow, 283 | let node = nodesForSection[indexPath.section]?[indexPath.row], 284 | let index = indexInParent(forNodeAt: indexPath), 285 | let dataSource = dataSource else { 286 | return nil 287 | } 288 | 289 | return dataSource.treeView(self, nodeForRowAt: index, forParentNode: node.parent) 290 | } 291 | 292 | /** 293 | Query the treeView to know if the node in parameter is expanded or not. 294 | 295 | - parameter node: The node you want to know the state. 296 | - parameter section: The section where the node is. 297 | 298 | - returns: Boolean value indicating if the node is expanded or not. 299 | */ 300 | public func isExpanded(node: TreeNodeProtocol, forSection section: Int) -> Bool { 301 | guard node.isExpandable, 302 | let nodes = nodesForSection[section], 303 | let treeNode = nodes.first(where: { $0.identifier == node.identifier }) else { return false } 304 | return treeNode.isExpanded 305 | } 306 | 307 | /** 308 | Insert a node at an indexPath in a parentNode. The indexPath must be relative to the new node's 309 | parent node passed in parameter. Your data source must be up to date to reflect this change 310 | immediately. 311 | 312 | - parameter indexPath: The index path where to insert the new row, relative to its parentNode. 313 | - parameter parentNode: The parent node where to insert the new row. If the parent is not expanded, 314 | the row will not be inserted visually. If the parentNode is nil, then the root will be considered. 315 | */ 316 | public func insertNode(at indexPath: IndexPath, inParent parentNode: TreeNodeProtocol?) { 317 | let section = indexPath.section 318 | guard let fullNewNode = dataSource?.treeView(self, nodeForRowAt: indexPath, forParentNode: parentNode), 319 | let realIndexPath = indexPathForNewNode(at: indexPath, in: parentNode), 320 | let indentationLevel = indentationLevelForChildren(inSection: section, of: parentNode) else { return } 321 | 322 | let newNode = MinimalTreeNode(identifier: fullNewNode.identifier) 323 | newNode.isExpandable = fullNewNode.isExpandable 324 | newNode.indentationLevel = indentationLevel 325 | newNode.parent = parentNode 326 | 327 | nodesForSection[indexPath.section]?.insert(newNode, at: realIndexPath.item) 328 | tableView.insertRows(at: [realIndexPath], with: .right) 329 | } 330 | 331 | /** 332 | This method will remove a node from the tree having the identifier passed in parameter in a given 333 | section. If the node is children of a not expanded parent, then the node will be deleted but no visual 334 | effect will be performed. If the node is a parent itself, all the children will be removed from the tree. 335 | 336 | - parameter identifier: The identifier of the node you want to remove from the tree. 337 | - parameter section: The section where the node exists. 338 | */ 339 | public func removeNode(withIdentifier identifier: String, inSection section: Int) { 340 | guard let indexPath = indexPathForNode(withIdentifier: identifier, inSection: section), 341 | var nodes = nodesForSection[section] else { return } 342 | 343 | var indexPaths = [indexPath] 344 | let minimalNode = nodes[indexPath.row] 345 | 346 | if minimalNode.isExpandable { 347 | if let range = closeNode(minimalNode, atIndex: indexPath.row, in: &nodes) { 348 | indexPaths += range.map { IndexPath(row: $0, section: section) } 349 | } 350 | nodesForSection[section] = nodes 351 | } 352 | 353 | nodesForSection[section]?.remove(at: indexPath.row) 354 | tableView.deleteRows(at: indexPaths, with: .right) 355 | } 356 | 357 | private func indentationLevelForChildren(inSection section: Int, of parent: TreeNodeProtocol?) -> Int? { 358 | var indentationLevel = 0 359 | if let parent = parent { 360 | guard parent.isExpandable, 361 | let parentIndexPath = indexPathForNode(parent, inSection: section), 362 | let minimalParentNode = nodesForSection[parentIndexPath.section]?[parentIndexPath.row] else { return nil } 363 | 364 | indentationLevel = minimalParentNode.indentationLevel + 1 365 | } 366 | return indentationLevel 367 | } 368 | 369 | private func indexPathForNewNode(at indexPath: IndexPath, in parent: TreeNodeProtocol?) -> IndexPath? { 370 | var indentationLevel = 0 371 | var realIndexPath = IndexPath(row: 0, section: indexPath.section) 372 | if let parent = parent { 373 | guard parent.isExpandable, 374 | let parentIndexPath = indexPathForNode(parent, inSection: indexPath.section), 375 | let minimalParentNode = nodesForSection[parentIndexPath.section]?[parentIndexPath.row], 376 | minimalParentNode.isExpanded else { return nil } 377 | 378 | realIndexPath = parentIndexPath 379 | indentationLevel = minimalParentNode.indentationLevel + 1 380 | } 381 | 382 | let targetIndex = realIndexPath.item + indexPath.item 383 | var currentIndex = realIndexPath.item 384 | while currentIndex < targetIndex { 385 | guard let node = nodesForSection[indexPath.section]?[currentIndex] else { return nil } 386 | guard node.indentationLevel == indentationLevel else { 387 | guard node.indentationLevel > indentationLevel else { break } 388 | continue 389 | } 390 | 391 | currentIndex += 1 392 | realIndexPath.item += 1 393 | } 394 | 395 | return realIndexPath 396 | } 397 | } 398 | 399 | //MARK: - UITableViewDataSource 400 | extension LNZTreeView: UITableViewDataSource { 401 | public func numberOfSections(in tableView: UITableView) -> Int { 402 | return dataSource?.numberOfSections(in: self) ?? 0 403 | } 404 | 405 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 406 | guard let rows = nodesForSection[section]?.count else { 407 | let rows = dataSource?.treeView(self, numberOfRowsInSection: section, forParentNode: nil) ?? 0 408 | 409 | var nodes = [MinimalTreeNode]() 410 | for i in 0.. UITableViewCell { 427 | guard let node = nodesForSection[indexPath.section]?[indexPath.row], 428 | let index = indexInParent(forNodeAt: indexPath) else { 429 | fatalError("Something wrong here") 430 | } 431 | 432 | //The logic of this view guarantees that if the node is a child, its parent will be not nil even if this is a placeholder. 433 | guard let cell = dataSource?.treeView(self, cellForRowAt: index, forParentNode: node.parent, isExpanded: node.isExpanded) else { 434 | fatalError("invalid dataSource for treeView: \(self)") 435 | } 436 | cell.indentationWidth = indentationWidth 437 | cell.indentationLevel = 2*node.indentationLevel 438 | 439 | return cell 440 | } 441 | 442 | private func indexInParent(forNodeAt indexPath: IndexPath) -> IndexPath? { 443 | guard let nodes = nodesForSection[indexPath.section] else { return nil } 444 | 445 | let node = nodes[indexPath.row] 446 | guard let index = nodes.filter({ node.parent?.identifier == $0.parent?.identifier }) 447 | .index(where: { node.identifier == $0.identifier }) else { return nil } 448 | return IndexPath(row: index, section: indexPath.section) 449 | } 450 | } 451 | 452 | //MARK: - UITableViewDelegate 453 | extension LNZTreeView: UITableViewDelegate { 454 | public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 455 | guard let nodes = nodesForSection[indexPath.section], 456 | let indexInParent = self.indexInParent(forNodeAt: indexPath) else { 457 | fatalError("Something wrong here") 458 | } 459 | let node = nodes[indexPath.row] 460 | 461 | return delegate?.treeView?(self, heightForNodeAt: indexInParent, forParentNode: node.parent) ?? tableView.rowHeight 462 | } 463 | 464 | public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 465 | guard let nodes = nodesForSection[indexPath.section], 466 | let indexInParent = self.indexInParent(forNodeAt: indexPath) else { 467 | fatalError("Something wrong here") 468 | } 469 | let node = nodes[indexPath.row] 470 | 471 | return delegate?.treeView?(self, canEditRowAt: indexInParent, forParentNode: node.parent) ?? false 472 | } 473 | 474 | public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { 475 | return .delete 476 | } 477 | 478 | public func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 479 | guard var nodes = nodesForSection[indexPath.section], 480 | let indexInParent = self.indexInParent(forNodeAt: indexPath) else { 481 | fatalError("Something wrong here") 482 | } 483 | let node = nodes[indexPath.row] 484 | delegate?.treeView?(self, commitDeleteForRowAt: indexInParent, forParentNode: node.parent) 485 | } 486 | 487 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 488 | guard var nodes = nodesForSection[indexPath.section], 489 | let indexInParent = self.indexInParent(forNodeAt: indexPath) else { 490 | fatalError("Something wrong here") 491 | } 492 | let node = nodes[indexPath.row] 493 | 494 | guard node.isExpandable else { 495 | delegate?.treeView?(self, didSelectNodeAt: indexInParent, forParentNode: node.parent) 496 | return 497 | } 498 | CATransaction.begin() 499 | tableView.beginUpdates() 500 | defer { 501 | CATransaction.commit() 502 | tableView.endUpdates() 503 | } 504 | 505 | tableView.reloadRows(at: [indexPath], with: .fade) 506 | 507 | if node.isExpanded { 508 | let range = closeNode(node, atIndex: indexPath.row, in: &nodes) 509 | nodesForSection[indexPath.section] = nodes 510 | 511 | if let deleteRange = range { 512 | //Updating the tableView 513 | let indexPaths = Array(deleteRange).map { IndexPath(row: $0, section: indexPath.section) } 514 | tableView.deleteRows(at: indexPaths, with: tableViewRowAnimation) 515 | } 516 | CATransaction.setCompletionBlock {[weak self] in 517 | guard let strongSelf = self else { return } 518 | strongSelf.delegate?.treeView?(strongSelf, didCollapseNodeAt: indexInParent, forParentNode: node.parent) 519 | } 520 | } else { 521 | let range = expandNode(node, at: indexPath, in: &nodes) 522 | nodesForSection[indexPath.section] = nodes 523 | 524 | if let insertRange = range { 525 | //Updating the tableView 526 | let indexPaths = Array(insertRange).map { IndexPath(row: $0, section: indexPath.section) } 527 | tableView.insertRows(at: indexPaths, with: tableViewRowAnimation) 528 | } 529 | CATransaction.setCompletionBlock {[weak self] in 530 | guard let strongSelf = self else { return } 531 | strongSelf.delegate?.treeView?(strongSelf, didExpandNodeAt: indexInParent, forParentNode: node.parent) 532 | } 533 | } 534 | } 535 | 536 | private func expandNode(_ node: MinimalTreeNode, at indexPath: IndexPath, in nodes: inout [MinimalTreeNode]) -> CountableClosedRange? { 537 | defer { node.isExpanded = true } 538 | guard let index = indexInParent(forNodeAt: indexPath), 539 | let fullNode = dataSource?.treeView(self, nodeForRowAt: index, forParentNode: node.parent) else { 540 | fatalError("invalid dataSource for treeView: \(self)") 541 | } 542 | 543 | let numberOfChildren = dataSource?.treeView(self, numberOfRowsInSection: indexPath.section, forParentNode: fullNode) ?? 0 544 | guard numberOfChildren > 0 else { return nil } 545 | var newNodes = [MinimalTreeNode]() 546 | for i in 0.. CountableClosedRange? { 563 | defer { node.isExpanded = false } 564 | 565 | guard index+1 < nodes.count else { return nil } 566 | 567 | //Updating the dataSource 568 | var nextNode = nodes[index + 1] 569 | var removedNodes = 0 570 | while nextNode.indentationLevel > node.indentationLevel { 571 | removedNodes += 1 572 | nodes.remove(at: index+1) 573 | guard index+1 < nodes.count else { 574 | //The array is over 575 | break 576 | } 577 | 578 | nextNode = nodes[index + 1] 579 | } 580 | 581 | guard removedNodes > 0 else { return nil } 582 | 583 | return index+1...index+removedNodes 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /LNZTreeView/LNZTreeViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LNZTreeViewDataSource.swift 3 | // PFRPG rd 4 | // 5 | // Created by Giuseppe Lanza on 24/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol LNZTreeViewDataSource: class { 12 | ///The tree view can be sectioned just like the UITableView 13 | func numberOfSections(in treeView: LNZTreeView) -> Int 14 | 15 | /** 16 | This method is indexed differently from a normal UITableView. The number of rows in a method call is 17 | dependant from the parent node parameter. If not nil, the parentNode indicates that treeView wants to 18 | know the number of children for the given parentNode, else the treeView is interested in root elements. 19 | 20 | - parameter treeView: The treeView asking for the number of rows. 21 | - parameter section: An index number identifying the section in treeView. 22 | - parameter parentNode: The TreeNode in which the treeView is interested in knowing its children count. 23 | If nil, the treeView is interested in the root for the section. 24 | 25 | - returns: An int value indicating the amount of nodes for a given parentNode 26 | */ 27 | func treeView(_ treeView: LNZTreeView, numberOfRowsInSection section: Int, forParentNode parentNode: TreeNodeProtocol?) -> Int 28 | 29 | /** 30 | To avoid duplication, the treeView will ask as needed the node for a certain indexPath. The indexPath 31 | is relative to the requested node's parent node. The parent node is passed in parameters. 32 | 33 | ## Example: 34 | An *indexPath* with row **i** and section **j** in parentNode **A** means the **i**th child of parentNode in the 35 | section **j**. If not present parentNode, the requested node is the ith element in the root of the section **j**. 36 | 37 | - parameter treeView: The treeView asking for the node. 38 | - parameter indexPath: The indexPath of the requested node. 39 | - parameter parentNode: The parentNode of the requested node. 40 | 41 | - returns: The requested node. 42 | */ 43 | func treeView(_ treeView: LNZTreeView, nodeForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> TreeNodeProtocol 44 | 45 | /** 46 | This method has to return the cell for a given node at a certain indexPath. The indexPath 47 | is relative to the requested node's parent node. The parent node is passed in parameters. 48 | 49 | ## Example: 50 | An *indexPath* with row **i** and section **j** in parentNode **A** means the **i**th child of parentNode in the 51 | section **j**. If not present parentNode, the requested node is the ith element in the root of the section **j**. 52 | 53 | - parameter treeView: The treeView asking for the node. 54 | - parameter indexPath: The indexPath of the requested node. 55 | - parameter parentNode: The parentNode of the requested node. 56 | 57 | - returns: The cell for the node at *indexPath*. 58 | */ 59 | func treeView(_ treeView: LNZTreeView, cellForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?, isExpanded: Bool) -> UITableViewCell 60 | } 61 | -------------------------------------------------------------------------------- /LNZTreeView/LNZTreeViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LNZTreeViewDelegate.swift 3 | // PFRPG rd 4 | // 5 | // Created by Giuseppe Lanza on 24/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc public protocol LNZTreeViewDelegate { 12 | 13 | @objc optional func treeView(_ treeView: LNZTreeView, canEditRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> Bool 14 | 15 | @objc optional func treeView(_ treeView: LNZTreeView, commitDeleteForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) 16 | 17 | @objc optional func treeView(_ treeView: LNZTreeView, heightForNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> CGFloat 18 | 19 | /** 20 | This method is called when a node is successfully expanded. The indexPath is relative to the 21 | *parentNode* parameter. 22 | 23 | ## Example: 24 | An *indexPath* with row **i** and section **j** in parentNode **A** means the **i**th child of parentNode in the 25 | section **j**. If not present parentNode, the requested node is the ith element in the root of the section **j**. 26 | 27 | - parameter treeView: The tree view on which the event was triggered. 28 | - parameter indexPath: The indexPath of the expanded node, relative to its *parentNode*. 29 | - parameter parentNode: The parentNode for the expanded node. If nil, root is to be intended. 30 | */ 31 | @objc optional func treeView(_ treeView: LNZTreeView, didExpandNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) 32 | 33 | /** 34 | This method is called when a node is successfully collapsed. The indexPath is relative to the 35 | *parentNode* parameter. 36 | 37 | ## Example: 38 | An *indexPath* with row **i** and section **j** in parentNode **A** means the **i**th child of parentNode in the 39 | section **j**. If not present parentNode, the requested node is the ith element in the root of the section **j**. 40 | 41 | - parameter treeView: The tree view on which the event was triggered. 42 | - parameter indexPath: The indexPath of the collapsed node, relative to its *parentNode*. 43 | - parameter parentNode: The parentNode for the collapsed node. If nil, root is to be intended. 44 | */ 45 | @objc optional func treeView(_ treeView: LNZTreeView, didCollapseNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) 46 | 47 | /** 48 | This method is called when a node is successfully selected. The indexPath is relative to the 49 | *parentNode* parameter. 50 | 51 | ## Example: 52 | An *indexPath* with row **i** and section **j** in parentNode **A** means the **i**th child of parentNode in the 53 | section **j**. If not present parentNode, the requested node is the ith element in the root of the section **j**. 54 | 55 | - parameter treeView: The tree view on which the event was triggered. 56 | - parameter indexPath: The indexPath of the selected node, relative to its *parentNode*. 57 | - parameter parentNode: The parentNode for the selected node. If nil, root is to be intended. 58 | */ 59 | @objc optional func treeView(_ treeView: LNZTreeView, didSelectNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) 60 | } 61 | -------------------------------------------------------------------------------- /LNZTreeViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 50D0A4131FB2215300DE8E02 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4121FB2215300DE8E02 /* AppDelegate.swift */; }; 11 | 50D0A4151FB2215300DE8E02 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4141FB2215300DE8E02 /* ViewController.swift */; }; 12 | 50D0A4181FB2215300DE8E02 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50D0A4161FB2215300DE8E02 /* Main.storyboard */; }; 13 | 50D0A41A1FB2215300DE8E02 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50D0A4191FB2215300DE8E02 /* Assets.xcassets */; }; 14 | 50D0A41D1FB2215300DE8E02 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50D0A41B1FB2215300DE8E02 /* LaunchScreen.storyboard */; }; 15 | 50D0A44B1FB22ABF00DE8E02 /* LNZTreeView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50D0A4421FB22ABF00DE8E02 /* LNZTreeView.framework */; }; 16 | 50D0A4541FB22ABF00DE8E02 /* LNZTreeView.h in Headers */ = {isa = PBXBuildFile; fileRef = 50D0A4441FB22ABF00DE8E02 /* LNZTreeView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | 50D0A4571FB22ABF00DE8E02 /* LNZTreeView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50D0A4421FB22ABF00DE8E02 /* LNZTreeView.framework */; }; 18 | 50D0A4581FB22ABF00DE8E02 /* LNZTreeView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 50D0A4421FB22ABF00DE8E02 /* LNZTreeView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 19 | 50D0A4601FB22ACF00DE8E02 /* LNZTreeViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4341FB2217D00DE8E02 /* LNZTreeViewDelegate.swift */; }; 20 | 50D0A4611FB22ACF00DE8E02 /* LNZTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4351FB2217D00DE8E02 /* LNZTreeView.swift */; }; 21 | 50D0A4621FB22ACF00DE8E02 /* LNZTreeViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4331FB2217D00DE8E02 /* LNZTreeViewDataSource.swift */; }; 22 | 50D0A4651FB22B2800DE8E02 /* LNZTreeViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4641FB22B2200DE8E02 /* LNZTreeViewTests.swift */; }; 23 | 50D0A4661FB22B3300DE8E02 /* TreeViewMockDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4381FB22A6300DE8E02 /* TreeViewMockDataSource.swift */; }; 24 | 50D0A4671FB22B3300DE8E02 /* TestNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A4391FB22A6300DE8E02 /* TestNode.swift */; }; 25 | 50D0A4681FB22B3300DE8E02 /* TreeViewMockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D0A43A1FB22A6300DE8E02 /* TreeViewMockDelegate.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 50D0A44C1FB22ABF00DE8E02 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 50D0A4071FB2215300DE8E02 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 50D0A4411FB22ABF00DE8E02; 34 | remoteInfo = LNZTreeView; 35 | }; 36 | 50D0A44E1FB22ABF00DE8E02 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 50D0A4071FB2215300DE8E02 /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 50D0A40E1FB2215300DE8E02; 41 | remoteInfo = LNZTreeViewDemo; 42 | }; 43 | 50D0A4551FB22ABF00DE8E02 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 50D0A4071FB2215300DE8E02 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 50D0A4411FB22ABF00DE8E02; 48 | remoteInfo = LNZTreeView; 49 | }; 50 | /* End PBXContainerItemProxy section */ 51 | 52 | /* Begin PBXCopyFilesBuildPhase section */ 53 | 50D0A45C1FB22ABF00DE8E02 /* Embed Frameworks */ = { 54 | isa = PBXCopyFilesBuildPhase; 55 | buildActionMask = 2147483647; 56 | dstPath = ""; 57 | dstSubfolderSpec = 10; 58 | files = ( 59 | 50D0A4581FB22ABF00DE8E02 /* LNZTreeView.framework in Embed Frameworks */, 60 | ); 61 | name = "Embed Frameworks"; 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXCopyFilesBuildPhase section */ 65 | 66 | /* Begin PBXFileReference section */ 67 | 50D0A40F1FB2215300DE8E02 /* LNZTreeViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LNZTreeViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 50D0A4121FB2215300DE8E02 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 69 | 50D0A4141FB2215300DE8E02 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 70 | 50D0A4171FB2215300DE8E02 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 71 | 50D0A4191FB2215300DE8E02 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | 50D0A41C1FB2215300DE8E02 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 73 | 50D0A41E1FB2215300DE8E02 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 50D0A4331FB2217D00DE8E02 /* LNZTreeViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNZTreeViewDataSource.swift; sourceTree = ""; }; 75 | 50D0A4341FB2217D00DE8E02 /* LNZTreeViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNZTreeViewDelegate.swift; sourceTree = ""; }; 76 | 50D0A4351FB2217D00DE8E02 /* LNZTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNZTreeView.swift; sourceTree = ""; }; 77 | 50D0A4381FB22A6300DE8E02 /* TreeViewMockDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeViewMockDataSource.swift; sourceTree = ""; }; 78 | 50D0A4391FB22A6300DE8E02 /* TestNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNode.swift; sourceTree = ""; }; 79 | 50D0A43A1FB22A6300DE8E02 /* TreeViewMockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeViewMockDelegate.swift; sourceTree = ""; }; 80 | 50D0A4421FB22ABF00DE8E02 /* LNZTreeView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LNZTreeView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 50D0A4441FB22ABF00DE8E02 /* LNZTreeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LNZTreeView.h; sourceTree = ""; }; 82 | 50D0A4451FB22ABF00DE8E02 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 83 | 50D0A44A1FB22ABF00DE8E02 /* LNZTreeViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LNZTreeViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 84 | 50D0A4531FB22ABF00DE8E02 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85 | 50D0A4641FB22B2200DE8E02 /* LNZTreeViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNZTreeViewTests.swift; sourceTree = ""; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | 50D0A40C1FB2215300DE8E02 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | 50D0A4571FB22ABF00DE8E02 /* LNZTreeView.framework in Frameworks */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | 50D0A43E1FB22ABF00DE8E02 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | 50D0A4471FB22ABF00DE8E02 /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | 50D0A44B1FB22ABF00DE8E02 /* LNZTreeView.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | /* End PBXFrameworksBuildPhase section */ 113 | 114 | /* Begin PBXGroup section */ 115 | 50D0A4061FB2215200DE8E02 = { 116 | isa = PBXGroup; 117 | children = ( 118 | 50D0A4111FB2215300DE8E02 /* LNZTreeViewDemo */, 119 | 50D0A4431FB22ABF00DE8E02 /* LNZTreeView */, 120 | 50D0A4501FB22ABF00DE8E02 /* LNZTreeViewTests */, 121 | 50D0A4101FB2215300DE8E02 /* Products */, 122 | ); 123 | sourceTree = ""; 124 | }; 125 | 50D0A4101FB2215300DE8E02 /* Products */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 50D0A40F1FB2215300DE8E02 /* LNZTreeViewDemo.app */, 129 | 50D0A4421FB22ABF00DE8E02 /* LNZTreeView.framework */, 130 | 50D0A44A1FB22ABF00DE8E02 /* LNZTreeViewTests.xctest */, 131 | ); 132 | name = Products; 133 | sourceTree = ""; 134 | }; 135 | 50D0A4111FB2215300DE8E02 /* LNZTreeViewDemo */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 50D0A4121FB2215300DE8E02 /* AppDelegate.swift */, 139 | 50D0A4141FB2215300DE8E02 /* ViewController.swift */, 140 | 50D0A4161FB2215300DE8E02 /* Main.storyboard */, 141 | 50D0A4191FB2215300DE8E02 /* Assets.xcassets */, 142 | 50D0A41B1FB2215300DE8E02 /* LaunchScreen.storyboard */, 143 | 50D0A41E1FB2215300DE8E02 /* Info.plist */, 144 | ); 145 | path = LNZTreeViewDemo; 146 | sourceTree = ""; 147 | }; 148 | 50D0A4371FB22A6300DE8E02 /* MockObjects */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 50D0A4381FB22A6300DE8E02 /* TreeViewMockDataSource.swift */, 152 | 50D0A4391FB22A6300DE8E02 /* TestNode.swift */, 153 | 50D0A43A1FB22A6300DE8E02 /* TreeViewMockDelegate.swift */, 154 | ); 155 | path = MockObjects; 156 | sourceTree = ""; 157 | }; 158 | 50D0A4431FB22ABF00DE8E02 /* LNZTreeView */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 50D0A4331FB2217D00DE8E02 /* LNZTreeViewDataSource.swift */, 162 | 50D0A4341FB2217D00DE8E02 /* LNZTreeViewDelegate.swift */, 163 | 50D0A4351FB2217D00DE8E02 /* LNZTreeView.swift */, 164 | 50D0A4441FB22ABF00DE8E02 /* LNZTreeView.h */, 165 | 50D0A4451FB22ABF00DE8E02 /* Info.plist */, 166 | ); 167 | path = LNZTreeView; 168 | sourceTree = ""; 169 | }; 170 | 50D0A4501FB22ABF00DE8E02 /* LNZTreeViewTests */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 50D0A4371FB22A6300DE8E02 /* MockObjects */, 174 | 50D0A4641FB22B2200DE8E02 /* LNZTreeViewTests.swift */, 175 | 50D0A4531FB22ABF00DE8E02 /* Info.plist */, 176 | ); 177 | path = LNZTreeViewTests; 178 | sourceTree = ""; 179 | }; 180 | /* End PBXGroup section */ 181 | 182 | /* Begin PBXHeadersBuildPhase section */ 183 | 50D0A43F1FB22ABF00DE8E02 /* Headers */ = { 184 | isa = PBXHeadersBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 50D0A4541FB22ABF00DE8E02 /* LNZTreeView.h in Headers */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXHeadersBuildPhase section */ 192 | 193 | /* Begin PBXNativeTarget section */ 194 | 50D0A40E1FB2215300DE8E02 /* LNZTreeViewDemo */ = { 195 | isa = PBXNativeTarget; 196 | buildConfigurationList = 50D0A42C1FB2215300DE8E02 /* Build configuration list for PBXNativeTarget "LNZTreeViewDemo" */; 197 | buildPhases = ( 198 | 50D0A40B1FB2215300DE8E02 /* Sources */, 199 | 50D0A40C1FB2215300DE8E02 /* Frameworks */, 200 | 50D0A40D1FB2215300DE8E02 /* Resources */, 201 | 50D0A45C1FB22ABF00DE8E02 /* Embed Frameworks */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | 50D0A4561FB22ABF00DE8E02 /* PBXTargetDependency */, 207 | ); 208 | name = LNZTreeViewDemo; 209 | productName = LNZTreeViewDemo; 210 | productReference = 50D0A40F1FB2215300DE8E02 /* LNZTreeViewDemo.app */; 211 | productType = "com.apple.product-type.application"; 212 | }; 213 | 50D0A4411FB22ABF00DE8E02 /* LNZTreeView */ = { 214 | isa = PBXNativeTarget; 215 | buildConfigurationList = 50D0A4591FB22ABF00DE8E02 /* Build configuration list for PBXNativeTarget "LNZTreeView" */; 216 | buildPhases = ( 217 | 50D0A43D1FB22ABF00DE8E02 /* Sources */, 218 | 50D0A43E1FB22ABF00DE8E02 /* Frameworks */, 219 | 50D0A43F1FB22ABF00DE8E02 /* Headers */, 220 | 50D0A4401FB22ABF00DE8E02 /* Resources */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | ); 226 | name = LNZTreeView; 227 | productName = LNZTreeView; 228 | productReference = 50D0A4421FB22ABF00DE8E02 /* LNZTreeView.framework */; 229 | productType = "com.apple.product-type.framework"; 230 | }; 231 | 50D0A4491FB22ABF00DE8E02 /* LNZTreeViewTests */ = { 232 | isa = PBXNativeTarget; 233 | buildConfigurationList = 50D0A45D1FB22ABF00DE8E02 /* Build configuration list for PBXNativeTarget "LNZTreeViewTests" */; 234 | buildPhases = ( 235 | 50D0A4461FB22ABF00DE8E02 /* Sources */, 236 | 50D0A4471FB22ABF00DE8E02 /* Frameworks */, 237 | 50D0A4481FB22ABF00DE8E02 /* Resources */, 238 | ); 239 | buildRules = ( 240 | ); 241 | dependencies = ( 242 | 50D0A44D1FB22ABF00DE8E02 /* PBXTargetDependency */, 243 | 50D0A44F1FB22ABF00DE8E02 /* PBXTargetDependency */, 244 | ); 245 | name = LNZTreeViewTests; 246 | productName = LNZTreeViewTests; 247 | productReference = 50D0A44A1FB22ABF00DE8E02 /* LNZTreeViewTests.xctest */; 248 | productType = "com.apple.product-type.bundle.unit-test"; 249 | }; 250 | /* End PBXNativeTarget section */ 251 | 252 | /* Begin PBXProject section */ 253 | 50D0A4071FB2215300DE8E02 /* Project object */ = { 254 | isa = PBXProject; 255 | attributes = { 256 | LastSwiftUpdateCheck = 0900; 257 | LastUpgradeCheck = 1000; 258 | ORGANIZATIONNAME = "Giuseppe Lanza"; 259 | TargetAttributes = { 260 | 50D0A40E1FB2215300DE8E02 = { 261 | CreatedOnToolsVersion = 9.0; 262 | LastSwiftMigration = 1000; 263 | ProvisioningStyle = Automatic; 264 | }; 265 | 50D0A4411FB22ABF00DE8E02 = { 266 | CreatedOnToolsVersion = 9.0; 267 | LastSwiftMigration = 1000; 268 | ProvisioningStyle = Automatic; 269 | }; 270 | 50D0A4491FB22ABF00DE8E02 = { 271 | CreatedOnToolsVersion = 9.0; 272 | LastSwiftMigration = 1000; 273 | ProvisioningStyle = Automatic; 274 | TestTargetID = 50D0A40E1FB2215300DE8E02; 275 | }; 276 | }; 277 | }; 278 | buildConfigurationList = 50D0A40A1FB2215300DE8E02 /* Build configuration list for PBXProject "LNZTreeViewDemo" */; 279 | compatibilityVersion = "Xcode 8.0"; 280 | developmentRegion = en; 281 | hasScannedForEncodings = 0; 282 | knownRegions = ( 283 | en, 284 | Base, 285 | ); 286 | mainGroup = 50D0A4061FB2215200DE8E02; 287 | productRefGroup = 50D0A4101FB2215300DE8E02 /* Products */; 288 | projectDirPath = ""; 289 | projectRoot = ""; 290 | targets = ( 291 | 50D0A40E1FB2215300DE8E02 /* LNZTreeViewDemo */, 292 | 50D0A4411FB22ABF00DE8E02 /* LNZTreeView */, 293 | 50D0A4491FB22ABF00DE8E02 /* LNZTreeViewTests */, 294 | ); 295 | }; 296 | /* End PBXProject section */ 297 | 298 | /* Begin PBXResourcesBuildPhase section */ 299 | 50D0A40D1FB2215300DE8E02 /* Resources */ = { 300 | isa = PBXResourcesBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | 50D0A41D1FB2215300DE8E02 /* LaunchScreen.storyboard in Resources */, 304 | 50D0A41A1FB2215300DE8E02 /* Assets.xcassets in Resources */, 305 | 50D0A4181FB2215300DE8E02 /* Main.storyboard in Resources */, 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | 50D0A4401FB22ABF00DE8E02 /* Resources */ = { 310 | isa = PBXResourcesBuildPhase; 311 | buildActionMask = 2147483647; 312 | files = ( 313 | ); 314 | runOnlyForDeploymentPostprocessing = 0; 315 | }; 316 | 50D0A4481FB22ABF00DE8E02 /* Resources */ = { 317 | isa = PBXResourcesBuildPhase; 318 | buildActionMask = 2147483647; 319 | files = ( 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | /* End PBXResourcesBuildPhase section */ 324 | 325 | /* Begin PBXSourcesBuildPhase section */ 326 | 50D0A40B1FB2215300DE8E02 /* Sources */ = { 327 | isa = PBXSourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | 50D0A4151FB2215300DE8E02 /* ViewController.swift in Sources */, 331 | 50D0A4131FB2215300DE8E02 /* AppDelegate.swift in Sources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | 50D0A43D1FB22ABF00DE8E02 /* Sources */ = { 336 | isa = PBXSourcesBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | 50D0A4601FB22ACF00DE8E02 /* LNZTreeViewDelegate.swift in Sources */, 340 | 50D0A4621FB22ACF00DE8E02 /* LNZTreeViewDataSource.swift in Sources */, 341 | 50D0A4611FB22ACF00DE8E02 /* LNZTreeView.swift in Sources */, 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | 50D0A4461FB22ABF00DE8E02 /* Sources */ = { 346 | isa = PBXSourcesBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | 50D0A4671FB22B3300DE8E02 /* TestNode.swift in Sources */, 350 | 50D0A4681FB22B3300DE8E02 /* TreeViewMockDelegate.swift in Sources */, 351 | 50D0A4651FB22B2800DE8E02 /* LNZTreeViewTests.swift in Sources */, 352 | 50D0A4661FB22B3300DE8E02 /* TreeViewMockDataSource.swift in Sources */, 353 | ); 354 | runOnlyForDeploymentPostprocessing = 0; 355 | }; 356 | /* End PBXSourcesBuildPhase section */ 357 | 358 | /* Begin PBXTargetDependency section */ 359 | 50D0A44D1FB22ABF00DE8E02 /* PBXTargetDependency */ = { 360 | isa = PBXTargetDependency; 361 | target = 50D0A4411FB22ABF00DE8E02 /* LNZTreeView */; 362 | targetProxy = 50D0A44C1FB22ABF00DE8E02 /* PBXContainerItemProxy */; 363 | }; 364 | 50D0A44F1FB22ABF00DE8E02 /* PBXTargetDependency */ = { 365 | isa = PBXTargetDependency; 366 | target = 50D0A40E1FB2215300DE8E02 /* LNZTreeViewDemo */; 367 | targetProxy = 50D0A44E1FB22ABF00DE8E02 /* PBXContainerItemProxy */; 368 | }; 369 | 50D0A4561FB22ABF00DE8E02 /* PBXTargetDependency */ = { 370 | isa = PBXTargetDependency; 371 | target = 50D0A4411FB22ABF00DE8E02 /* LNZTreeView */; 372 | targetProxy = 50D0A4551FB22ABF00DE8E02 /* PBXContainerItemProxy */; 373 | }; 374 | /* End PBXTargetDependency section */ 375 | 376 | /* Begin PBXVariantGroup section */ 377 | 50D0A4161FB2215300DE8E02 /* Main.storyboard */ = { 378 | isa = PBXVariantGroup; 379 | children = ( 380 | 50D0A4171FB2215300DE8E02 /* Base */, 381 | ); 382 | name = Main.storyboard; 383 | sourceTree = ""; 384 | }; 385 | 50D0A41B1FB2215300DE8E02 /* LaunchScreen.storyboard */ = { 386 | isa = PBXVariantGroup; 387 | children = ( 388 | 50D0A41C1FB2215300DE8E02 /* Base */, 389 | ); 390 | name = LaunchScreen.storyboard; 391 | sourceTree = ""; 392 | }; 393 | /* End PBXVariantGroup section */ 394 | 395 | /* Begin XCBuildConfiguration section */ 396 | 50D0A42A1FB2215300DE8E02 /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ALWAYS_SEARCH_USER_PATHS = NO; 400 | CLANG_ANALYZER_NONNULL = YES; 401 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 402 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 403 | CLANG_CXX_LIBRARY = "libc++"; 404 | CLANG_ENABLE_MODULES = YES; 405 | CLANG_ENABLE_OBJC_ARC = YES; 406 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 407 | CLANG_WARN_BOOL_CONVERSION = YES; 408 | CLANG_WARN_COMMA = YES; 409 | CLANG_WARN_CONSTANT_CONVERSION = YES; 410 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 411 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 412 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 413 | CLANG_WARN_EMPTY_BODY = YES; 414 | CLANG_WARN_ENUM_CONVERSION = YES; 415 | CLANG_WARN_INFINITE_RECURSION = YES; 416 | CLANG_WARN_INT_CONVERSION = YES; 417 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 418 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 419 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 420 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 421 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 422 | CLANG_WARN_STRICT_PROTOTYPES = YES; 423 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 424 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 425 | CLANG_WARN_UNREACHABLE_CODE = YES; 426 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 427 | CODE_SIGN_IDENTITY = "iPhone Developer"; 428 | COPY_PHASE_STRIP = NO; 429 | DEBUG_INFORMATION_FORMAT = dwarf; 430 | ENABLE_STRICT_OBJC_MSGSEND = YES; 431 | ENABLE_TESTABILITY = YES; 432 | GCC_C_LANGUAGE_STANDARD = gnu11; 433 | GCC_DYNAMIC_NO_PIC = NO; 434 | GCC_NO_COMMON_BLOCKS = YES; 435 | GCC_OPTIMIZATION_LEVEL = 0; 436 | GCC_PREPROCESSOR_DEFINITIONS = ( 437 | "DEBUG=1", 438 | "$(inherited)", 439 | ); 440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_UNDECLARED_SELECTOR = YES; 443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 444 | GCC_WARN_UNUSED_FUNCTION = YES; 445 | GCC_WARN_UNUSED_VARIABLE = YES; 446 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 447 | MTL_ENABLE_DEBUG_INFO = YES; 448 | ONLY_ACTIVE_ARCH = YES; 449 | SDKROOT = iphoneos; 450 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 451 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 452 | }; 453 | name = Debug; 454 | }; 455 | 50D0A42B1FB2215300DE8E02 /* Release */ = { 456 | isa = XCBuildConfiguration; 457 | buildSettings = { 458 | ALWAYS_SEARCH_USER_PATHS = NO; 459 | CLANG_ANALYZER_NONNULL = YES; 460 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 461 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 462 | CLANG_CXX_LIBRARY = "libc++"; 463 | CLANG_ENABLE_MODULES = YES; 464 | CLANG_ENABLE_OBJC_ARC = YES; 465 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 466 | CLANG_WARN_BOOL_CONVERSION = YES; 467 | CLANG_WARN_COMMA = YES; 468 | CLANG_WARN_CONSTANT_CONVERSION = YES; 469 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 470 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 471 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 472 | CLANG_WARN_EMPTY_BODY = YES; 473 | CLANG_WARN_ENUM_CONVERSION = YES; 474 | CLANG_WARN_INFINITE_RECURSION = YES; 475 | CLANG_WARN_INT_CONVERSION = YES; 476 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 477 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 478 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 479 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 480 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 481 | CLANG_WARN_STRICT_PROTOTYPES = YES; 482 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 483 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 484 | CLANG_WARN_UNREACHABLE_CODE = YES; 485 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 486 | CODE_SIGN_IDENTITY = "iPhone Developer"; 487 | COPY_PHASE_STRIP = NO; 488 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 489 | ENABLE_NS_ASSERTIONS = NO; 490 | ENABLE_STRICT_OBJC_MSGSEND = YES; 491 | GCC_C_LANGUAGE_STANDARD = gnu11; 492 | GCC_NO_COMMON_BLOCKS = YES; 493 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 494 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 495 | GCC_WARN_UNDECLARED_SELECTOR = YES; 496 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 497 | GCC_WARN_UNUSED_FUNCTION = YES; 498 | GCC_WARN_UNUSED_VARIABLE = YES; 499 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 500 | MTL_ENABLE_DEBUG_INFO = NO; 501 | SDKROOT = iphoneos; 502 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 503 | VALIDATE_PRODUCT = YES; 504 | }; 505 | name = Release; 506 | }; 507 | 50D0A42D1FB2215300DE8E02 /* Debug */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 511 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 512 | CODE_SIGN_STYLE = Automatic; 513 | DEVELOPMENT_TEAM = ZJ8CN8P2Z5; 514 | INFOPLIST_FILE = LNZTreeViewDemo/Info.plist; 515 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 516 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 517 | PRODUCT_BUNDLE_IDENTIFIER = net.pfrpg.LNZTreeViewDemo; 518 | PRODUCT_NAME = "$(TARGET_NAME)"; 519 | SWIFT_VERSION = 4.2; 520 | TARGETED_DEVICE_FAMILY = "1,2"; 521 | }; 522 | name = Debug; 523 | }; 524 | 50D0A42E1FB2215300DE8E02 /* Release */ = { 525 | isa = XCBuildConfiguration; 526 | buildSettings = { 527 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 528 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 529 | CODE_SIGN_STYLE = Automatic; 530 | DEVELOPMENT_TEAM = ZJ8CN8P2Z5; 531 | INFOPLIST_FILE = LNZTreeViewDemo/Info.plist; 532 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 533 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 534 | PRODUCT_BUNDLE_IDENTIFIER = net.pfrpg.LNZTreeViewDemo; 535 | PRODUCT_NAME = "$(TARGET_NAME)"; 536 | SWIFT_VERSION = 4.2; 537 | TARGETED_DEVICE_FAMILY = "1,2"; 538 | }; 539 | name = Release; 540 | }; 541 | 50D0A45A1FB22ABF00DE8E02 /* Debug */ = { 542 | isa = XCBuildConfiguration; 543 | buildSettings = { 544 | CODE_SIGN_IDENTITY = ""; 545 | CODE_SIGN_STYLE = Automatic; 546 | CURRENT_PROJECT_VERSION = 1; 547 | DEFINES_MODULE = YES; 548 | DEVELOPMENT_TEAM = ZJ8CN8P2Z5; 549 | DYLIB_COMPATIBILITY_VERSION = 1; 550 | DYLIB_CURRENT_VERSION = 1; 551 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 552 | INFOPLIST_FILE = LNZTreeView/Info.plist; 553 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 554 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 555 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 556 | PRODUCT_BUNDLE_IDENTIFIER = net.pfrpg.LNZTreeView; 557 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 558 | SKIP_INSTALL = YES; 559 | SWIFT_VERSION = 4.2; 560 | TARGETED_DEVICE_FAMILY = "1,2"; 561 | VERSIONING_SYSTEM = "apple-generic"; 562 | VERSION_INFO_PREFIX = ""; 563 | }; 564 | name = Debug; 565 | }; 566 | 50D0A45B1FB22ABF00DE8E02 /* Release */ = { 567 | isa = XCBuildConfiguration; 568 | buildSettings = { 569 | CODE_SIGN_IDENTITY = ""; 570 | CODE_SIGN_STYLE = Automatic; 571 | CURRENT_PROJECT_VERSION = 1; 572 | DEFINES_MODULE = YES; 573 | DEVELOPMENT_TEAM = ZJ8CN8P2Z5; 574 | DYLIB_COMPATIBILITY_VERSION = 1; 575 | DYLIB_CURRENT_VERSION = 1; 576 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 577 | INFOPLIST_FILE = LNZTreeView/Info.plist; 578 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 579 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 580 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 581 | PRODUCT_BUNDLE_IDENTIFIER = net.pfrpg.LNZTreeView; 582 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 583 | SKIP_INSTALL = YES; 584 | SWIFT_VERSION = 4.2; 585 | TARGETED_DEVICE_FAMILY = "1,2"; 586 | VERSIONING_SYSTEM = "apple-generic"; 587 | VERSION_INFO_PREFIX = ""; 588 | }; 589 | name = Release; 590 | }; 591 | 50D0A45E1FB22ABF00DE8E02 /* Debug */ = { 592 | isa = XCBuildConfiguration; 593 | buildSettings = { 594 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 595 | CLANG_ENABLE_MODULES = YES; 596 | CODE_SIGN_STYLE = Automatic; 597 | DEVELOPMENT_TEAM = ZJ8CN8P2Z5; 598 | INFOPLIST_FILE = LNZTreeViewTests/Info.plist; 599 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 600 | PRODUCT_BUNDLE_IDENTIFIER = net.pfrpg.LNZTreeViewTests; 601 | PRODUCT_NAME = "$(TARGET_NAME)"; 602 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 603 | SWIFT_VERSION = 4.2; 604 | TARGETED_DEVICE_FAMILY = "1,2"; 605 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LNZTreeViewDemo.app/LNZTreeViewDemo"; 606 | }; 607 | name = Debug; 608 | }; 609 | 50D0A45F1FB22ABF00DE8E02 /* Release */ = { 610 | isa = XCBuildConfiguration; 611 | buildSettings = { 612 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 613 | CLANG_ENABLE_MODULES = YES; 614 | CODE_SIGN_STYLE = Automatic; 615 | DEVELOPMENT_TEAM = ZJ8CN8P2Z5; 616 | INFOPLIST_FILE = LNZTreeViewTests/Info.plist; 617 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 618 | PRODUCT_BUNDLE_IDENTIFIER = net.pfrpg.LNZTreeViewTests; 619 | PRODUCT_NAME = "$(TARGET_NAME)"; 620 | SWIFT_VERSION = 4.2; 621 | TARGETED_DEVICE_FAMILY = "1,2"; 622 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LNZTreeViewDemo.app/LNZTreeViewDemo"; 623 | }; 624 | name = Release; 625 | }; 626 | /* End XCBuildConfiguration section */ 627 | 628 | /* Begin XCConfigurationList section */ 629 | 50D0A40A1FB2215300DE8E02 /* Build configuration list for PBXProject "LNZTreeViewDemo" */ = { 630 | isa = XCConfigurationList; 631 | buildConfigurations = ( 632 | 50D0A42A1FB2215300DE8E02 /* Debug */, 633 | 50D0A42B1FB2215300DE8E02 /* Release */, 634 | ); 635 | defaultConfigurationIsVisible = 0; 636 | defaultConfigurationName = Release; 637 | }; 638 | 50D0A42C1FB2215300DE8E02 /* Build configuration list for PBXNativeTarget "LNZTreeViewDemo" */ = { 639 | isa = XCConfigurationList; 640 | buildConfigurations = ( 641 | 50D0A42D1FB2215300DE8E02 /* Debug */, 642 | 50D0A42E1FB2215300DE8E02 /* Release */, 643 | ); 644 | defaultConfigurationIsVisible = 0; 645 | defaultConfigurationName = Release; 646 | }; 647 | 50D0A4591FB22ABF00DE8E02 /* Build configuration list for PBXNativeTarget "LNZTreeView" */ = { 648 | isa = XCConfigurationList; 649 | buildConfigurations = ( 650 | 50D0A45A1FB22ABF00DE8E02 /* Debug */, 651 | 50D0A45B1FB22ABF00DE8E02 /* Release */, 652 | ); 653 | defaultConfigurationIsVisible = 0; 654 | defaultConfigurationName = Release; 655 | }; 656 | 50D0A45D1FB22ABF00DE8E02 /* Build configuration list for PBXNativeTarget "LNZTreeViewTests" */ = { 657 | isa = XCConfigurationList; 658 | buildConfigurations = ( 659 | 50D0A45E1FB22ABF00DE8E02 /* Debug */, 660 | 50D0A45F1FB22ABF00DE8E02 /* Release */, 661 | ); 662 | defaultConfigurationIsVisible = 0; 663 | defaultConfigurationName = Release; 664 | }; 665 | /* End XCConfigurationList section */ 666 | }; 667 | rootObject = 50D0A4071FB2215300DE8E02 /* Project object */; 668 | } 669 | -------------------------------------------------------------------------------- /LNZTreeViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LNZTreeViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LNZTreeViewDemo.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LNZTreeViewDemo.xcodeproj/xcshareddata/xcschemes/LNZTreeView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /LNZTreeViewDemo.xcodeproj/xcuserdata/giuseppelanza.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LNZTreeView.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | LNZTreeViewDemo.xcscheme 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 50D0A40E1FB2215300DE8E02 21 | 22 | primary 23 | 24 | 25 | 50D0A4411FB22ABF00DE8E02 26 | 27 | primary 28 | 29 | 30 | 50D0A4491FB22ABF00DE8E02 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LNZTreeViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LNZTreeViewDemo 4 | // 5 | // Created by Giuseppe Lanza on 07/11/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /LNZTreeViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /LNZTreeViewDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /LNZTreeViewDemo/Assets.xcassets/index_folder_indicator.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "index_folder_indicator.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /LNZTreeViewDemo/Assets.xcassets/index_folder_indicator.imageset/index_folder_indicator.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gringoireDM/LNZTreeView/4cf95c2dcdf7a4368c9a34dd270fdc3482397d2c/LNZTreeViewDemo/Assets.xcassets/index_folder_indicator.imageset/index_folder_indicator.pdf -------------------------------------------------------------------------------- /LNZTreeViewDemo/Assets.xcassets/index_folder_indicator_open.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "index_folder_indicator_open.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /LNZTreeViewDemo/Assets.xcassets/index_folder_indicator_open.imageset/index_folder_indicator_open.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gringoireDM/LNZTreeView/4cf95c2dcdf7a4368c9a34dd270fdc3482397d2c/LNZTreeViewDemo/Assets.xcassets/index_folder_indicator_open.imageset/index_folder_indicator_open.pdf -------------------------------------------------------------------------------- /LNZTreeViewDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LNZTreeViewDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /LNZTreeViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.1.2 19 | CFBundleVersion 20 | 9 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /LNZTreeViewDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // LNZTreeViewDemo 4 | // 5 | // Created by Giuseppe Lanza on 07/11/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LNZTreeView 11 | 12 | class CustomUITableViewCell: UITableViewCell 13 | { 14 | override func layoutSubviews() { 15 | super.layoutSubviews(); 16 | 17 | guard var imageFrame = imageView?.frame else { return } 18 | 19 | let offset = CGFloat(indentationLevel) * indentationWidth 20 | imageFrame.origin.x += offset 21 | imageView?.frame = imageFrame 22 | } 23 | } 24 | 25 | 26 | class Node: NSObject, TreeNodeProtocol { 27 | var identifier: String 28 | var isExpandable: Bool { 29 | return children != nil 30 | } 31 | 32 | var children: [Node]? 33 | 34 | init(withIdentifier identifier: String, andChildren children: [Node]? = nil) { 35 | self.identifier = identifier 36 | self.children = children 37 | } 38 | } 39 | 40 | class ViewController: UIViewController { 41 | 42 | @IBOutlet weak var treeView: LNZTreeView! 43 | var root = [Node]() 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | treeView.register(CustomUITableViewCell.self, forCellReuseIdentifier: "cell") 49 | 50 | treeView.tableViewRowAnimation = .right 51 | treeView.keyboardDismissMode = .none 52 | generateRandomNodes() 53 | treeView.resetTree() 54 | } 55 | 56 | func generateRandomNodes() { 57 | let depth = 3 58 | let rootSize = 30 59 | 60 | var root: [Node]! 61 | 62 | var lastLevelNodes: [Node]? 63 | for i in 0.. [Node] { 86 | let nodes = Array(0.. Node in 87 | return Node(withIdentifier: "\(arc4random()%UInt32.max)") 88 | } 89 | 90 | return nodes 91 | } 92 | 93 | override func didReceiveMemoryWarning() { 94 | super.didReceiveMemoryWarning() 95 | // Dispose of any resources that can be recreated. 96 | } 97 | } 98 | 99 | extension ViewController: LNZTreeViewDataSource { 100 | func numberOfSections(in treeView: LNZTreeView) -> Int { 101 | return 1 102 | } 103 | 104 | func treeView(_ treeView: LNZTreeView, numberOfRowsInSection section: Int, forParentNode parentNode: TreeNodeProtocol?) -> Int { 105 | guard let parent = parentNode as? Node else { 106 | return root.count 107 | } 108 | 109 | return parent.children?.count ?? 0 110 | } 111 | 112 | func treeView(_ treeView: LNZTreeView, nodeForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> TreeNodeProtocol { 113 | guard let parent = parentNode as? Node else { 114 | return root[indexPath.row] 115 | } 116 | 117 | return parent.children![indexPath.row] 118 | } 119 | 120 | func treeView(_ treeView: LNZTreeView, cellForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?, isExpanded: Bool) -> UITableViewCell { 121 | 122 | let node: Node 123 | if let parent = parentNode as? Node { 124 | node = parent.children![indexPath.row] 125 | } else { 126 | node = root[indexPath.row] 127 | } 128 | 129 | let cell = treeView.dequeueReusableCell(withIdentifier: "cell", for: node, inSection: indexPath.section) 130 | 131 | if node.isExpandable { 132 | if isExpanded { 133 | cell.imageView?.image = #imageLiteral(resourceName: "index_folder_indicator_open") 134 | } else { 135 | cell.imageView?.image = #imageLiteral(resourceName: "index_folder_indicator") 136 | } 137 | } else { 138 | cell.imageView?.image = nil 139 | } 140 | 141 | cell.textLabel?.text = node.identifier 142 | 143 | return cell 144 | } 145 | } 146 | 147 | extension ViewController: LNZTreeViewDelegate { 148 | func treeView(_ treeView: LNZTreeView, heightForNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> CGFloat { 149 | return 60 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /LNZTreeViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LNZTreeViewTests/LNZTreeViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LNZTreeViewTests.swift 3 | // PFRPG rdTests 4 | // 5 | // Created by Giuseppe Lanza on 24/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import LNZTreeView 11 | 12 | class LNZTreeViewTests: XCTestCase { 13 | let treeView = LNZTreeView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 14 | let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 15 | let dataSource = TreeViewMockDataSource() 16 | let delegate = TreeViewMockDelegate() 17 | 18 | func testNoDataSource() { 19 | window.addSubview(treeView) 20 | XCTAssertEqual(treeView.numberOfSections(), 0) 21 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), 0) 22 | } 23 | 24 | func testLoadRoot() { 25 | let roots: [[TestNode]] = Array(0..<100).map { (i) -> [TestNode] in 26 | return Array(0..<100).map { (j) -> TestNode in 27 | return TestNode(identifier: "sec\(i)_row\(j)", isExpandable: false, children: nil) 28 | } 29 | } 30 | dataSource.roots = roots 31 | treeView.dataSource = dataSource 32 | 33 | window.addSubview(treeView) 34 | 35 | XCTAssertEqual(treeView.numberOfSections(), roots.count) 36 | for (i, root) in roots.enumerated() { 37 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(i), root.count) 38 | } 39 | } 40 | 41 | func testOpenEmptyNode() { 42 | let node = TestNode(identifier: "sec0_row0", isExpandable: true, children: nil) 43 | dataSource.roots = [[node]] 44 | delegate.roots = [[node]] 45 | 46 | treeView.dataSource = dataSource 47 | treeView.delegate = delegate 48 | 49 | window.addSubview(treeView) 50 | XCTAssertEqual(treeView.numberOfSections(), 1) 51 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), 1) 52 | 53 | let expect = expectation(description: "expandWaiter") 54 | delegate.expectation = expect 55 | 56 | let expanded = treeView.expand(node: node, inSection: 0) 57 | XCTAssertTrue(expanded) 58 | 59 | waitForExpectations(timeout: 1, handler: nil) 60 | 61 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), 1) 62 | 63 | XCTAssertEqual(delegate.expandedNodes, [node]) 64 | XCTAssertTrue(delegate.collapsedNodes.isEmpty) 65 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 66 | } 67 | 68 | func testOpenNode() { 69 | let childrenNodes = Array(0..<100).map { (j) -> TestNode in 70 | return TestNode(identifier: "child_sec0_row\(j)", isExpandable: false, children: nil) 71 | } 72 | let nodes = [ 73 | TestNode(identifier: "sec0_row0", isExpandable: true, children: childrenNodes), 74 | TestNode(identifier: "sec0_row1", isExpandable: false, children: nil) 75 | ] 76 | 77 | dataSource.roots = [nodes] 78 | delegate.roots = [nodes] 79 | 80 | treeView.dataSource = dataSource 81 | treeView.delegate = delegate 82 | 83 | window.addSubview(treeView) 84 | 85 | XCTAssertEqual(treeView.numberOfSections(), 1) 86 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count) 87 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[0]), 0) 88 | 89 | let expect = expectation(description: "expandWaiter") 90 | delegate.expectation = expect 91 | 92 | let expanded = treeView.expand(node: nodes[0], inSection: 0) 93 | XCTAssertTrue(expanded) 94 | 95 | waitForExpectations(timeout: 1, handler: nil) 96 | 97 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count + childrenNodes.count) 98 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[0]), childrenNodes.count) 99 | 100 | XCTAssertEqual(delegate.expandedNodes, [nodes[0]]) 101 | XCTAssertTrue(delegate.collapsedNodes.isEmpty) 102 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 103 | 104 | } 105 | 106 | func testCloseNode() { 107 | let childrenNodes = Array(0..<100).map { (j) -> TestNode in 108 | return TestNode(identifier: "child_sec0_row\(j)", isExpandable: false, children: nil) 109 | } 110 | let nodes = [ 111 | TestNode(identifier: "sec0_row0", isExpandable: true, children: childrenNodes), 112 | TestNode(identifier: "sec0_row1", isExpandable: false, children: nil) 113 | ] 114 | 115 | dataSource.roots = [nodes] 116 | delegate.roots = [nodes] 117 | 118 | treeView.dataSource = dataSource 119 | treeView.delegate = delegate 120 | 121 | window.addSubview(treeView) 122 | 123 | var expect = expectation(description: "expandWaiter") 124 | delegate.expectation = expect 125 | 126 | let expanded = treeView.expand(node: nodes[0], inSection: 0) 127 | XCTAssertTrue(expanded) 128 | 129 | waitForExpectations(timeout: 1, handler: nil) 130 | 131 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count + childrenNodes.count) 132 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[0]), (nodes[0].children?.count ?? 0)) 133 | 134 | expect = expectation(description: "collapseWaiter") 135 | delegate.expectation = expect 136 | 137 | 138 | let closed = treeView.collapse(node: nodes[0], inSection: 0) 139 | XCTAssertTrue(closed) 140 | 141 | waitForExpectations(timeout: 1, handler: nil) 142 | 143 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count) 144 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[0]), 0) 145 | 146 | XCTAssertEqual(delegate.expandedNodes, [nodes[0]]) 147 | XCTAssertEqual(delegate.collapsedNodes, [nodes[0]]) 148 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 149 | 150 | } 151 | 152 | func testOpenMultipleNodes() { 153 | let childrenNodes = Array(0..<100).map { (j) -> TestNode in 154 | return TestNode(identifier: "child_sec0_row\(j)", isExpandable: false, children: nil) 155 | } 156 | let nodes = [ 157 | TestNode(identifier: "sec0_row0", isExpandable: true, children: childrenNodes), 158 | TestNode(identifier: "sec0_row1", isExpandable: true, children: childrenNodes) 159 | ] 160 | 161 | dataSource.roots = [nodes] 162 | delegate.roots = [nodes] 163 | 164 | treeView.dataSource = dataSource 165 | treeView.delegate = delegate 166 | 167 | window.addSubview(treeView) 168 | var expanded = treeView.expand(node: nodes[0], inSection: 0) 169 | XCTAssertTrue(expanded) 170 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count + (nodes[0].children?.count ?? 0)) 171 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[0]), (nodes[0].children?.count ?? 0)) 172 | 173 | expanded = treeView.expand(node: nodes[1], inSection: 0) 174 | XCTAssertTrue(expanded) 175 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count + (nodes[0].children?.count ?? 0) + (nodes[1].children?.count ?? 0)) 176 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[1]), (nodes[1].children?.count ?? 0)) 177 | } 178 | 179 | func testCloseMultipleNodes() { 180 | let childrenNodes = Array(0..<100).map { (j) -> TestNode in 181 | return TestNode(identifier: "child_sec0_row\(j)", isExpandable: false, children: nil) 182 | } 183 | let nodes = [ 184 | TestNode(identifier: "sec0_row0", isExpandable: true, children: childrenNodes), 185 | TestNode(identifier: "sec0_row1", isExpandable: true, children: childrenNodes) 186 | ] 187 | 188 | dataSource.roots = [nodes] 189 | delegate.roots = [nodes] 190 | 191 | treeView.dataSource = dataSource 192 | treeView.delegate = delegate 193 | 194 | window.addSubview(treeView) 195 | 196 | var expect = expectation(description: "expandWaiter") 197 | delegate.expectation = expect 198 | 199 | treeView.expand(node: nodes[0], inSection: 0) 200 | 201 | waitForExpectations(timeout: 1, handler: nil) 202 | 203 | expect = expectation(description: "expandWaiter") 204 | delegate.expectation = expect 205 | 206 | treeView.expand(node: nodes[1], inSection: 0) 207 | 208 | waitForExpectations(timeout: 1, handler: nil) 209 | 210 | expect = expectation(description: "collapseWaiter") 211 | delegate.expectation = expect 212 | 213 | var collapsed = treeView.collapse(node: nodes[1], inSection: 0) 214 | XCTAssertTrue(collapsed) 215 | 216 | waitForExpectations(timeout: 1, handler: nil) 217 | 218 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count + (nodes[0].children?.count ?? 0)) 219 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[1]), 0) 220 | 221 | expect = expectation(description: "collapseWaiter") 222 | delegate.expectation = expect 223 | 224 | collapsed = treeView.collapse(node: nodes[0], inSection: 0) 225 | XCTAssertTrue(collapsed) 226 | 227 | waitForExpectations(timeout: 1, handler: nil) 228 | 229 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), nodes.count) 230 | XCTAssertEqual(treeView.numberOfNodesForSection(0, inParent: nodes[0]), 0) 231 | 232 | XCTAssertEqual(delegate.expandedNodes, nodes) 233 | XCTAssertEqual(delegate.collapsedNodes, [nodes[1], nodes[0]]) 234 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 235 | } 236 | 237 | func testDoubleOpenNode() { 238 | let node = TestNode(identifier: "sec0_row0", isExpandable: true, children: nil) 239 | dataSource.roots = [[node]] 240 | delegate.roots = [[node]] 241 | 242 | treeView.dataSource = dataSource 243 | treeView.delegate = delegate 244 | 245 | window.addSubview(treeView) 246 | 247 | let expect = expectation(description: "expandWaiter") 248 | delegate.expectation = expect 249 | 250 | var expanded = treeView.expand(node: node, inSection: 0) 251 | waitForExpectations(timeout: 1, handler: nil) 252 | 253 | 254 | expanded = treeView.expand(node: node, inSection: 0) 255 | XCTAssertTrue(expanded) 256 | 257 | XCTAssertEqual(treeView.numberOfTotalNodesInSection(0), 1) 258 | 259 | XCTAssertEqual(delegate.expandedNodes, [node]) 260 | XCTAssertTrue(delegate.collapsedNodes.isEmpty) 261 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 262 | } 263 | 264 | func testCloseEmptyNode() { 265 | let node = TestNode(identifier: "sec0_row0", isExpandable: true, children: nil) 266 | dataSource.roots = [[node]] 267 | delegate.roots = [[node]] 268 | 269 | treeView.dataSource = dataSource 270 | treeView.delegate = delegate 271 | 272 | window.addSubview(treeView) 273 | 274 | var expect = expectation(description: "expandWaiter") 275 | delegate.expectation = expect 276 | 277 | let expanded = treeView.expand(node: node, inSection: 0) 278 | XCTAssertTrue(expanded) 279 | 280 | waitForExpectations(timeout: 1, handler: nil) 281 | 282 | expect = expectation(description: "expandWaiter") 283 | delegate.expectation = expect 284 | 285 | let collapsed = treeView.collapse(node: node, inSection: 0) 286 | XCTAssertTrue(collapsed) 287 | 288 | waitForExpectations(timeout: 1, handler: nil) 289 | XCTAssertEqual(delegate.expandedNodes, [node]) 290 | XCTAssertEqual(delegate.collapsedNodes, [node]) 291 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 292 | } 293 | 294 | func testSelectExpandable() { 295 | let node = TestNode(identifier: "sec0_row0", isExpandable: true, children: nil) 296 | dataSource.roots = [[node]] 297 | delegate.roots = [[node]] 298 | 299 | treeView.dataSource = dataSource 300 | treeView.delegate = delegate 301 | 302 | window.addSubview(treeView) 303 | 304 | var expect = expectation(description: "expandWaiter") 305 | delegate.expectation = expect 306 | 307 | var selected = treeView.select(node: node, inSection: 0) 308 | XCTAssertTrue(selected) 309 | 310 | waitForExpectations(timeout: 1, handler: nil) 311 | 312 | XCTAssertEqual(delegate.expandedNodes, [node]) 313 | XCTAssertTrue(delegate.collapsedNodes.isEmpty) 314 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 315 | 316 | expect = expectation(description: "expandWaiter") 317 | delegate.expectation = expect 318 | 319 | selected = treeView.select(node: node, inSection: 0) 320 | XCTAssertTrue(selected) 321 | 322 | waitForExpectations(timeout: 1, handler: nil) 323 | 324 | XCTAssertEqual(delegate.expandedNodes, [node]) 325 | XCTAssertEqual(delegate.collapsedNodes, [node]) 326 | XCTAssertTrue(delegate.selectedNodes.isEmpty) 327 | } 328 | 329 | func testSelectNotExpandable() { 330 | let node = TestNode(identifier: "sec0_row0", isExpandable: false, children: nil) 331 | dataSource.roots = [[node]] 332 | delegate.roots = [[node]] 333 | 334 | treeView.dataSource = dataSource 335 | treeView.delegate = delegate 336 | 337 | window.addSubview(treeView) 338 | 339 | let selected = treeView.select(node: node, inSection: 0) 340 | XCTAssertTrue(selected) 341 | 342 | XCTAssertTrue(delegate.expandedNodes.isEmpty) 343 | XCTAssertTrue(delegate.collapsedNodes.isEmpty) 344 | XCTAssertEqual(delegate.selectedNodes, [node]) 345 | 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /LNZTreeViewTests/MockObjects/TestNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestNode.swift 3 | // PFRPG rdTests 4 | // 5 | // Created by Giuseppe Lanza on 24/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import LNZTreeView 11 | 12 | protocol ExpandableNode: TreeNodeProtocol { 13 | var children: [ExpandableNode]? { get } 14 | } 15 | 16 | class TestNode: ExpandableNode, Equatable { 17 | static func ==(lhs: TestNode, rhs: TestNode) -> Bool { 18 | return lhs.identifier == rhs.identifier 19 | } 20 | 21 | var identifier: String 22 | var isExpandable: Bool 23 | var children: [ExpandableNode]? 24 | 25 | init(identifier: String, isExpandable: Bool, children: [ExpandableNode]?) { 26 | self.identifier = identifier 27 | self.isExpandable = isExpandable 28 | self.children = children 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LNZTreeViewTests/MockObjects/TreeViewMockDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewMockDataSource.swift 3 | // PFRPG rdTests 4 | // 5 | // Created by Giuseppe Lanza on 24/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import LNZTreeView 11 | 12 | class TreeViewMockDataSource: LNZTreeViewDataSource { 13 | var roots: [[T]]! 14 | 15 | func numberOfSections(in treeView: LNZTreeView) -> Int { 16 | return roots.count 17 | } 18 | 19 | func treeView(_ treeView: LNZTreeView, numberOfRowsInSection section: Int, forParentNode parentNode: TreeNodeProtocol?) -> Int { 20 | guard let parent = parentNode as? T else { return roots[section].count } 21 | return parent.children?.count ?? 0 22 | } 23 | 24 | func treeView(_ treeView: LNZTreeView, nodeForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> TreeNodeProtocol { 25 | guard let parent = parentNode as? T else { return roots[indexPath.section][indexPath.row] } 26 | return parent.children![indexPath.row] 27 | } 28 | 29 | func treeView(_ treeView: LNZTreeView, cellForRowAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?, isExpanded: Bool) -> UITableViewCell { 30 | let cell = UITableViewCell() 31 | return cell 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LNZTreeViewTests/MockObjects/TreeViewMockDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewMockDelegate.swift 3 | // PFRPG rdTests 4 | // 5 | // Created by Giuseppe Lanza on 24/09/2017. 6 | // Copyright © 2017 Giuseppe Lanza. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import LNZTreeView 12 | 13 | class TreeViewMockDelegate: LNZTreeViewDelegate { 14 | var roots: [[T]]! 15 | 16 | var expandedNodes = [T]() 17 | var collapsedNodes = [T]() 18 | 19 | var selectedNodes = [T]() 20 | 21 | var expectation: XCTestExpectation? 22 | 23 | func node(at indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) -> T { 24 | var node: T! 25 | if let parent = parentNode as? T { 26 | node = (parent.children![indexPath.row] as! T) 27 | } else { 28 | node = roots[indexPath.section][indexPath.row] 29 | } 30 | return node 31 | } 32 | 33 | func treeView(_ treeView: LNZTreeView, didExpandNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) { 34 | expandedNodes.append(node(at: indexPath, forParentNode: parentNode)) 35 | expectation?.fulfill() 36 | } 37 | 38 | func treeView(_ treeView: LNZTreeView, didCollapseNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) { 39 | collapsedNodes.append(node(at: indexPath, forParentNode: parentNode)) 40 | expectation?.fulfill() 41 | } 42 | 43 | func treeView(_ treeView: LNZTreeView, didSelectNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNodeProtocol?) { 44 | selectedNodes.append(node(at: indexPath, forParentNode: parentNode)) 45 | expectation?.fulfill() 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/gringoireDM/LNZTreeView.svg?branch=master)](https://travis-ci.org/gringoireDM/LNZTreeView) [![codecov](https://codecov.io/gh/gringoireDM/LNZTreeView/branch/master/graph/badge.svg)](https://codecov.io/gh/gringoireDM/LNZTreeView) [![Swift](https://img.shields.io/badge/swift-4.2-orange.svg)](https://swift.org) 2 | 3 | # LNZTreeView 4 | This is a swift implementation for iOS of a Tree View. A Tree View is a graphical representation of a tree. Each element (node) can have a number of sub elements (children). 5 | 6 | This particular implementation of TreeView organizes nodes and subnodes in rows and each node has an indentation that indicates the hierarchy of the element. A parent can be expanded or collapsed and each children can be a parent itself containing more sub nodes. 7 | 8 | ![LNZTreeView](./LNZTreeView.gif) 9 | 10 | ## Basic Principles 11 | The LNZTreeView is a view built on top of UITableView. It proxies UITableView's delegate and datasource in order to have a hierarchical usage of those APIs. 12 | 13 | It can work with any entity conforming the protocol `TreeNode`. 14 | 15 | ### Cells 16 | Just like for UITableView it is possible to use Custom Cells in a `LNZTreeView`. You can register a cell class to be used for a specific cell Identifier by using the methods `register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)` and `register(_ nib: UINib?, forCellReuseIdentifier identifier: String)`. 17 | 18 | Just like for `UITableView` it is possible to obtain an `UITableViewCell` instance of the type previously specified with the register methods by using the `dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell` and `dequeueReusableCell(withIdentifier identifier: String) -> UITableViewCell?` methods. 19 | 20 | ### Delegate and DataSource 21 | The DataSource is the responsible of populating the TreeView according with your set of data. For efficiency, the LNZTreeView will query its data source if needed to get the right info to render the rows. All the indexPaths in the data source's methods are relative to the parameter **parentNode**. 22 | 23 | For example: 24 | ``` 25 | root 26 | --- element A 27 | --- --- sub element A 28 | --- --- sub element B 29 | --- --- sub element C 30 | --- element B 31 | ``` 32 | 33 | In this case an IndexPath with row 1 in **parentNode** equals to `element A`, will represent `sub element B`. The section of IndexPath just like for UITableView represent the index of the section to be rendered. If **parentNode** is nil, then the indexPath is relative to the root. 34 | 35 | Example: 36 | ``` 37 | root A 38 | --- element AA 39 | --- --- sub element AA 40 | --- --- sub element AB 41 | --- --- sub element AC 42 | --- element AB 43 | root B 44 | ``` 45 | 46 | In this case an IndexPath with row 1 in **parentNode** equals to nil, will represent `root B`. 47 | 48 | All the indexPaths in the delegate works following the same logic. 49 | 50 | If selected a node will automatically expand if it is expandable or it will collapse if already expanded. The delegate will be aknowledged of this events by the methods `treeView(_ treeView: LNZTreeView, didExpandNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNode?)` and `treeView(_ treeView: LNZTreeView, didCollapseNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNode?)`. If the node is not expandable (or collapsable) the method `treeView(_ treeView: LNZTreeView, didSelectNodeAt indexPath: IndexPath, forParentNode parentNode: TreeNode?)` will be called. 51 | 52 | ## Install 53 | 54 | You can install LNZTreeView via cocoapods 55 | 56 | ``` 57 | use_frameworks! 58 | pod 'LNZTreeView' 59 | ``` 60 | --------------------------------------------------------------------------------