├── .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 | [](https://travis-ci.org/gringoireDM/LNZTreeView) [](https://codecov.io/gh/gringoireDM/LNZTreeView) [](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 | 
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 |
--------------------------------------------------------------------------------