iOS의 UITableView에서 드롭 다운 목록


Answers

테이블 뷰 셀을 통해 구현하는 경우 더 쉽고 자연스러운 방법입니다. 확장 셀보기, 섹션 헤더 없음, 단순한 셀 (우리는 결국 테이블 뷰에 있음).

디자인은 다음과 같습니다.

  • MVVM 접근 방식을 사용하여 셀을 구성하는 데 필요한 정보를 보유하는 CollapsableViewModel 클래스를 만듭니다. label, image
  • 위의 인스턴스 외에도 CollapsableViewModel 객체의 배열 인 children 과 드롭 다운의 상태를 보유하는 isCollapsed 라는 두 개의 추가 필드가 있습니다.
  • 뷰 컨트롤러에는 CollapsableViewModel 의 계층 구조에 대한 참조뿐만 아니라 화면에 렌더링 될 뷰 모델을 포함하는 플랫 목록 ( displayedRows 속성)이 있습니다.
  • 셀이 도청 될 때마다 자식이 있는지 여부를 확인하고 insertRowsAtIndexPaths()deleteRowsAtIndexPaths() 함수를 통해 deleteRowsAtIndexPaths() 및 테이블보기에서 행을 추가 또는 제거합니다.

Swift 코드는 다음과 같습니다 (코드가 뷰 모델의 label 속성 만 사용하여 깨끗하게 유지됩니다).

import UIKit

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool

    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed
    }
}

class CollapsableTableViewController: UITableViewController {
    let data = [
        CollapsableViewModel(label: "Account", image: nil, children: [
            CollapsableViewModel(label: "Profile"),
            CollapsableViewModel(label: "Activate account"),
            CollapsableViewModel(label: "Change password")]),
        CollapsableViewModel(label: "Group"),
        CollapsableViewModel(label: "Events", image: nil, children: [
            CollapsableViewModel(label: "Nearby"),
            CollapsableViewModel(label: "Global"),
            ]),
        CollapsableViewModel(label: "Deals"),
    ]

    var displayedRows: [CollapsableViewModel] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        displayedRows = data
    }

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return displayedRows.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier") ?? UITableViewCell()
        let viewModel = displayedRows[indexPath.row]
        cell.textLabel!.text = viewModel.label
        return cell
    }

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: false)
        let viewModel = displayedRows[indexPath.row]
        if viewModel.children.count > 0 {
            let range = indexPath.row+1...indexPath.row+viewModel.children.count
            let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
            tableView.beginUpdates()
            if viewModel.isCollapsed {
                displayedRows.insertContentsOf(viewModel.children, at: indexPath.row+1)
                tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
            } else {
                displayedRows.removeRange(range)
                tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
            }
            tableView.endUpdates()
        }
        viewModel.isCollapsed = !viewModel.isCollapsed
    }
}

Objective-C 버전은 쉽게 번역 할 수 있으며, 더 짧고 읽기 쉽기 때문에 Swift 버전을 추가했습니다.

몇 가지 작은 변경 사항을 사용하면 코드를 사용하여 여러 수준의 드롭 다운 목록을 생성 할 수 있습니다.

편집하다

사람들은 분리 기호에 대해 물어 보았습니다. 뷰 모델로 구성되는 사용자 정의 클래스 CollapsibleTableViewCell 을 추가하면됩니다 (마지막으로 컨트롤러에서 셀 구성 논리를 셀의 속으로 이동 - 셀). 셀의 일부에 대해서만 구분 기호 논리에 대한 크레딧 사람 질문에 대답에 간다.

먼저 모델을 업데이트하고 테이블 뷰 셀에 렌더링을 표시할지 또는 구분 기호를 표시하지 needsSeparator 알려주는 needsSeparator 속성을 추가합니다.

class CollapsibleViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsibleViewModel]
    var isCollapsed: Bool
    var needsSeparator: Bool = true

    init(label: String, image: UIImage? = nil, children: [CollapsibleViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed

        for child in self.children {
            child.needsSeparator = false
        }
        self.children.last?.needsSeparator = true
    }
}

그런 다음 셀 클래스를 추가합니다.

class CollapsibleTableViewCell: UITableViewCell {
    let separator = UIView(frame: CGRectZero)

    func configure(viewModel: CollapsibleViewModel) {
        self.textLabel?.text = viewModel.label
        if(viewModel.needsSeparator) {
            separator.backgroundColor = UIColor.grayColor()
            contentView.addSubview(separator)
        } else {
             separator.removeFromSuperview()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        let separatorHeight = 1 / UIScreen.mainScreen().scale
        separator.frame = CGRectMake(separatorInset.left, contentView.bounds.height - separatorHeight, contentView.bounds.width-separatorInset.left-separatorInset.right, separatorHeight)
    }
}

그런 다음 cellForRowAtIndexPath 수정하여 이러한 종류의 셀을 반환해야합니다.

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = (tableView.dequeueReusableCellWithIdentifier("CollapsibleTableViewCell") as? CollapsibleTableViewCell) ?? CollapsibleTableViewCell(style: .Default, reuseIdentifier: "CollapsibleTableViewCell")
    cell.configure(displayedRows[indexPath.row])
    return cell
}

마지막으로 xib 또는 코드 ( tableView.separatorStyle = .None )에서 기본 표보기 셀 구분 기호를 제거하십시오.

Question

iOS에서이 유형의 테이블 뷰를 만드는 방법 ??

여기에서 첫 번째 행 '계정'을 탭하면 이미지에 표시되는 행이 더 많아지면서 자동으로 스크롤됩니다. 다시 계정을 탭하면 해당보기가 숨겨집니다.




일반적으로 행 높이를 설정하여 처리합니다. 예를 들어, 드롭 다운 목록이있는 두 개의 메뉴 항목이 있습니다.

  • 메뉴 1
    • 항목 1.1
    • 항목 1.2
    • 항목 1.3
  • 메뉴 2
    • 항목 2.1
    • 항목 2.2

따라서 2 개의 섹션으로 된 테이블 뷰를 생성해야합니다. 첫 번째 섹션은 4 개의 행 (메뉴 1과 그 항목)을 포함하고 초 섹션은 3 개의 행 (메뉴 2와 그 항목)을 포함합니다.

섹션의 첫 번째 행에만 항상 높이를 설정합니다. 그리고 사용자가 첫 번째 행을 클릭하면 높이를 설정하고이 섹션을 다시로드하여이 섹션 행을 확장합니다.




iOS 프레임 워크 ( UIKit)의 보기와 같은 트리보기 용 컨트롤은 기본적으로 제공되지 않습니다. 다른 사용자가 지적했듯이 (외부 라이브러리를 사용하지 않고) 가장 간단한 해결책은 원하는 동작을 모방하기 위해 UITableView 의 대리자 및 데이터 소스에 사용자 지정 논리를 추가하는 것입니다.

다행스럽게도 확장 / 축소 작업의 세부 사항을 신경 쓰지 않고 원하는 트리 뷰를 구현할 수있는 오픈 소스 라이브러리가 있습니다. iOS 플랫폼에서 사용할 수있는 몇 가지가 있습니다 . 대부분의 경우 이러한 라이브러리는 UITableView 랩핑하고 프로그래머가 쉽게 사용할 수있는 인터페이스를 제공하므로 트리 뷰의 구현 세부 사항이 아닌 문제에 집중할 수 있습니다.

필자는 개인적 으로 iOS에서 뷰와 같은 트리 뷰를 생성하는 데 필요한 비용을 최소화하기 위해 RATreeView 라이브러리를 작성했습니다. 예제 프로젝트 ( Objective-CSwift 에서 사용 가능)를 점검하여이 컨트롤의 작동 방식 및 작동 방식을 확인할 수 있습니다. 내 컨트롤을 사용하면 원하는보기를 만드는 것이 매우 간단합니다.

  1. DataObject 구조체는 트리 뷰 노드에 대한 정보를 유지하는 데 사용됩니다. 셀의 제목, 이미지 (셀에 이미지가있는 경우) 및 자식 (셀에 자식이있는 경우)에 대한 정보를 유지 관리합니다.
class DataObject
{
    let name : String
    let imageURL : NSURL?
    private(set) var children : [DataObject]

    init(name : String, imageURL : NSURL?, children: [DataObject]) {
        self.name = name
        self.imageURL = imageURL
        self.children = children
    }

    convenience init(name : String) {
        self.init(name: name, imageURL: nil, children: [DataObject]())
    }
}
  1. 프로토콜 TreeTableViewCell 을 선언하고 해당 프로토콜을 준수하는 두 개의 셀을 구현합니다. 이 셀 중 하나는 루트 항목을 표시하는 데 사용되고 다른 하나는 루트 항목의 하위 항목을 표시하는 데 사용됩니다.
protocol TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool)
}

class ChildTreeTableViewCell : UITableViewCell, TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
       //implementation goes here 
    }
}

class RootTreeTableViewCell : UITableViewCell, TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
       //implementation goes here
    }
}
  1. Out View Controller (MVC) 또는 View Model (MVVM)에서 우리는 트리 뷰의 백업을 담당하는 데이터 구조를 정의합니다.
let profileDataObject = DataObject(name: "Profile")
let privateAccountDataObject = DataObject(name: "Private Account")
let changePasswordDataObject = DataObject(name: "Change Password")
let accountDataObject = DataObject(name: "Account", imageURL: NSURL(string: "AccountImage"), children: [profileDataObject, privateAccountDataObject, changePasswordDataObject])

let groupDataObject = DataObject(name: "Group", imageURL: NSURL(string: "GroupImage"), children: [])
let eventDataObject = DataObject(name: "Event", imageURL: NSURL(string: "EventImage"), children: [])
let dealsDataObject = DataObject(name: "Deals", imageURL: NSURL(string: "DealsImage"), children: [])

data = [accountDataObject, groupDataObject, eventDataObject, dealsDataObject]
  1. 다음으로 우리는 RATreeView 의 데이터 소스로부터 몇 가지 메소드를 구현해야 할 것이다.
func treeView(treeView: RATreeView, numberOfChildrenOfItem item: AnyObject?) -> Int {
    if let item = item as? DataObject {
        return item.children.count //return number of children of specified item
    } else {
        return self.data.count //return number of top level items here
    }
}

func treeView(treeView: RATreeView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
    if let item = item as? DataObject {
        return item.children[index] //we return child of specified item here (using provided `index` variable)
    } else {
        return data[index] as AnyObject //we return root item here (using provided `index` variable)
    }
}

func treeView(treeView: RATreeView, cellForItem item: AnyObject?) -> UITableViewCell {
    let cellIdentifier = item ? TreeTableViewChildCell : TreeTableViewCellRootCell
    let cell = treeView.dequeueReusableCellWithIdentifier(cellIdentifier) as! TreeTableViewCell

    //TreeTableViewCell is a protocol which is implemented by two kinds of
    //cells - the one responsible for root items in the tree view and another 
    //one responsible for children. As we use protocol we don't care
    //which one is truly being used here. Both of them can be
    //configured using data from `DataItem` object.

    let item = item as! DataObject
    let isExpanded = treeView.isCellForItemExpanded(item) //this variable can be used to adjust look of the cell by determining whether cell is expanded or not

    cell.setup(withTitle: item.name, imageURL: item.imageURL, expanded: isExpanded)

    return cell
}

내 라이브러리를 사용하면 셀을 확장하거나 축소 할 필요가 없다는 것을 알 수 있습니다.이 라이브러리는 RATreeView 처리합니다. 셀을 구성하는 데 사용되는 데이터에 대해서만 책임이 있으며 나머지는 컨트롤 자체에서 처리됩니다.




외부 라이브러리를 사용하고 싶지 않다면 사용자 정의 셀을 2 개 만들 수 있습니다. 하나는 확장 전, 다른 하나는 확장 후 (다른 식별자로) 보여줍니다. 셀을 클릭하면 셀이 확장되었는지 확인합니다. 그렇지 않은 경우에는 확장 된 셀 식별자를 사용하고 확장되지 않은 셀 식별자를 사용하십시오.

확장 된 테이블 뷰 셀을 만드는 가장 좋은 방법입니다.




Collapsable TableView가 필요합니다. 이를 달성하기 위해 TableView에서 축소 된 (축소 된) 섹션과 확장 된 섹션을 추적해야합니다. 이를 위해, 확장 된 섹션의 인덱스 세트를 유지해야하며, 부울 배열은 각 섹션의 값이 해당 섹션이 확장되었는지 여부를 나타냅니다. 특정 행에 높이를 지정하는 동안 특정 색인에서 값을 확인하십시오. 자세한 내용은 이 링크 를 확인하십시오.

여기서 단면 표보기에 대해 배울 수 있습니다.

Github에는 서드 파티 라이브러리가있어 허스트에서 구할 수 있습니다. CollapsableTableView 또는 CollapsableTable-Swift를 살펴보십시오.




나는 @Cristik 솔루션을 좋아한다. 얼마 전에 나는 똑같은 문제를 겪었다. 나의 솔루션은 같은 원리를 따른다. 그래서 이것은 제가 가지고있는 요구 사항에 근거하여 제가 제안한 것입니다 :

  1. 좀 더 일반화하기 위해 테이블의 항목은 확장 기능에 특화된 클래스에서 상속 받아서는 안되며 필요한 속성을 정의하는 프로토콜이 있어야합니다

  2. 우리가 확장 할 수있는 레벨의 수에는 제한이 없어야합니다. 따라서 테이블에는 옵션, 하위 옵션, 하위 하위 옵션 등이있을 수 있습니다.

  3. 테이블 뷰는 일반적인 애니메이션 ( reloadData 없음) 중 하나를 사용하여 셀을 표시하거나 숨겨야합니다.

  4. 확장 동작이 반드시 셀을 선택하는 사용자에게 부착되어서는 안되며, 셀은 예를 들어 UISwitch를 가질 수 있습니다

간단한 구현 버전 ( https://github.com/JuanjoArreola/ExpandableCells )은 다음과 같습니다.

먼저 프로토콜 :

protocol CellDescriptor: class {
    var count: Int { get }
    var identifier: String! { get }
}

확장 가능하지 않은 셀의 수는 항상 1입니다.

extension CellDescriptor {
    var count: Int { return 1 }
}

그런 다음 확장 가능한 셀 프로토콜 :

protocol ExpandableCellDescriptor: CellDescriptor {
    var active: Bool { get set }
    var children: [CellDescriptor] { get set }

    subscript(index: Int) -> CellDescriptor? { get }
    func indexOf(cellDescriptor: CellDescriptor) -> Int?
}

빠른 것에 대한 멋진 점은 프로토콜 확장에서 일부 구현을 작성할 수 있고 준수하는 모든 클래스가 기본 구현을 사용할 수 있으므로 count 하위 subscriptindexOf 구현을 추가로 작성하고 다음과 같은 몇 가지 유용한 함수를 추가로 작성할 수 있다는 점입니다.

extension ExpandableCellDescriptor {
    var count: Int {
        var total = 1
        if active {
            children.forEach({ total += $0.count })
        }
        return total
    }

    var countIfActive: Int {
        ...
    }

    subscript(index: Int) -> CellDescriptor? {
        ...
    }

    func indexOf(cellDescriptor: CellDescriptor) -> Int? {
        ...
    }

    func append(cellDescriptor: CellDescriptor) {
        children.append(cellDescriptor)
    }
}

전체 구현은 CellDescriptor.swift 파일에 있습니다.

또한 동일한 파일에는 ExpandableCellDescriptor 를 구현하고 자체적으로 셀을 표시하지 않는 CellDescriptionArray 라는 클래스가 있습니다

이제 모든 클래스는 특정 클래스에서 상속 할 필요없이 이전 프로토콜을 준수 할 수 있습니다. github의 예제 코드에서는 OptionExpandableOption 이라는 두 가지 클래스를 만들었습니다. ExpandableOption 은 다음과 같습니다.

class ExpandableOption: ExpandableCellDescriptor {

    var delegate: ExpandableCellDelegate?

    var identifier: String!
    var active: Bool = false {
        didSet {
            delegate?.expandableCell(self, didChangeActive: active)
        }
    }

    var children: [CellDescriptor] = []
    var title: String?
}

그리고 이것은 UITableViewCell 하위 클래스 중 하나입니다.

class SwitchTableViewCell: UITableViewCell, CellDescrptionConfigurable {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var switchControl: UISwitch!

    var cellDescription: CellDescriptor! {
        didSet {
            if let option = cellDescription as? ExpandableOption {
                titleLabel.text = option.title
                switchControl.on = option.active
            }
        }
    }

    @IBAction func activeChanged(sender: UISwitch) {
       let expandableCellDescriptor = cellDescription as! ExpandableCellDescriptor
       expandableCellDescriptor.active = sender.on
    }
}

셀 및 클래스를 원하는대로 구성 할 수 있으며 이미지, 레이블, 스위치 등을 추가 할 수 있습니다. 제한이 없으며 필요한 프로토콜을 변경할 필요가 없습니다.

마지막으로 TableViewController에서 옵션 트리를 만듭니다.

var options = CellDescriptionArray()

override func viewDidLoad() {
   super.viewDidLoad()

   let account = ExpandableOption(identifier: "ExpandableCell", title: "Account")
   let profile = Option(identifier: "SimpleCell", title: "Profile")
   let isPublic = ExpandableOption(identifier: "SwitchCell", title: "Public")
   let caption = Option(identifier: "SimpleCell", title: "Anyone can see this account")
   isPublic.append(caption)
   account.append(profile)
   account.append(isPublic)
   options.append(account)

   let group = ExpandableOption(identifier: "ExpandableCell", title: "Group")
   group.append(Option(identifier: "SimpleCell", title: "Group Settings"))
   options.append(group)
   ...
}

나머지 구현은 이제 매우 간단합니다.

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return options.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
   let option = options[indexPath.row]!
   let cell = tableView.dequeueReusableCellWithIdentifier(option.identifier, forIndexPath: indexPath)

   (cell as! CellDescrptionConfigurable).cellDescription = option
   (option as? ExpandCellInformer)?.delegate = self
   return cell
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    guard let option = options[indexPath.row] else { return }
    guard let expandableOption = option as? ExpandableOption else { return }
    if expandableOption.identifier == "ExpandableCell" {
        expandableOption.active = !expandableOption.active
    }
}

func expandableCell(expandableCell: ExpandableCellDescriptor, didChangeActive active: Bool) {
    guard let index = options.indexOf(expandableCell) else { return }
    var indexPaths = [NSIndexPath]()
    for row in 1..<expandableCell.countIfActive {
        indexPaths.append(NSIndexPath(forRow: index + row, inSection: 0))
    }
    if active {
        tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
    } else {
        tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
    }
}

그것은 많은 코드처럼 보일지도 모른다. 그러나 대부분은 테이블 뷰를 그리는 데 필요한 정보의 대부분이 CellDescriptor.swift 파일에 올바르게 존재하고, 셀 구성 코드가 UITableViewCell 하위 클래스 내에 있으며, 상대적으로 TableViewController 자체에는 코드가 거의 없습니다.

희망이 도움이됩니다.