I would like to implement custom List-like view in SwiftUI that has to enable more feature than standard native List view in SwiftUI. I want to add drag and drop which doesn't exist in List (in spite of onMove()).
I have implemented this list in such way:
import SwiftUI
import MobileCoreServices
final class ReorderIndexPath: NSIndexPath {
}
extension ReorderIndexPath : NSItemProviderWriting {
public static var writableTypeIdentifiersForItemProvider: [String] {
return [kUTTypeData as String]
}
public func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: #escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
progress.completedUnitCount = 100
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
return progress
}
}
extension ReorderIndexPath : NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
return [kUTTypeData as String]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> ReorderIndexPath {
do {
return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! ReorderIndexPath
} catch {
fatalError(error.localizedDescription)
}
}
}
struct ReorderableList: UIViewControllerRepresentable {
struct Model {
private(set) var items : [AnyView]
init(items: [AnyView]) {
self.items = items
}
mutating func addItem(_ item: AnyView, at index: Int) {
items.insert(item, at: index)
}
mutating func removeItem(at index: Int) {
items.remove(at: index)
}
mutating func moveItem(at sourceIndex: Int, to destinationIndex: Int) {
guard sourceIndex != destinationIndex else { return }
let item = items[sourceIndex]
items.remove(at: sourceIndex)
items.insert(item, at: destinationIndex)
}
func canHandle(_ session: UIDropSession) -> Bool {
return session.canLoadObjects(ofClass: ReorderIndexPath.self)
}
func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
//let item = items[indexPath.row]
//let data = item.data(using: .utf8)
let itemProvider = NSItemProvider()
itemProvider.registerObject(ReorderIndexPath(row: indexPath.row, section: indexPath.section), visibility: .all)
return [
UIDragItem(itemProvider: itemProvider)
]
}
}
#State private var model : Model
// MARK: - Actions
let onReorder : (Int, Int) -> Void
let onDelete : ((Int) -> Bool)?
// MARK: - Init
public init<Data, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _, _ in }, onDelete: ((Int) -> Bool)? = nil, _ content: #escaping () -> ForEach<Data, Data.Element.ID, RowContent>) where Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable {
let content = content()
var items = [AnyView]()
content.data.forEach { element in
let item = content.content(element)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<Data, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Data, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data) { element in HStack { rowContent(element) } }
}
}
public init<Data, ID, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ content: #escaping () -> ForEach<Data, ID, RowContent>) where Data : RandomAccessCollection, ID : Hashable, RowContent : View {
let content = content()
var items = [AnyView]()
content.data.forEach { element in
let item = content.content(element)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<Data, ID, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Data, id: KeyPath<Data.Element, ID>, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Data : RandomAccessCollection, ID : Hashable, RowContent : View {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data, id: id) { element in HStack { rowContent(element) } }
}
}
public init<RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ content: #escaping () -> ForEach<Range<Int>, Int, RowContent>) where RowContent : View {
let content = content()
var items = [AnyView]()
content.data.forEach { i in
let item = content.content(i)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<RowContent>(onReorder: #escaping (Int, Int) -> Void = {_,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Range<Int>, #ViewBuilder rowContent: #escaping (Int) -> RowContent) where RowContent : View {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data) { i in
HStack { rowContent(i) }
}
}
}
func makeUIViewController(context: Context) -> UITableViewController {
let tableView = UITableViewController()
tableView.tableView.delegate = context.coordinator
tableView.tableView.dataSource = context.coordinator
tableView.tableView.dragInteractionEnabled = true
tableView.tableView.dragDelegate = context.coordinator
tableView.tableView.dropDelegate = context.coordinator
tableView.tableView.register(HostingTableViewCell<AnyView>.self, forCellReuseIdentifier: "HostingCell")
context.coordinator.controller = tableView
return tableView
}
func updateUIViewController(_ uiView: UITableViewController, context: Context) {
//print("Reorderable list update")
//uiView.tableView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource, UITableViewDragDelegate, UITableViewDropDelegate {
let parent: ReorderableList
weak var controller : UITableViewController?
// MARK: - Init
init(_ parent: ReorderableList) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.model.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell<AnyView>
let item = parent.model.items[indexPath.row]
cell.host(item, parent: controller!)
return cell
}
// MARK: - Delegate
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return parent.onDelete != nil ? .delete : .none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if parent.onDelete?(indexPath.row) ?? false {
tableView.beginUpdates()
parent.model.removeItem(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
} else if editingStyle == .insert {
}
}
/*
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = parent.model.items[sourceIndexPath.row]
parent.model.items.remove(at: sourceIndexPath.row)
parent.model.items.insert(object, at: destinationIndexPath.row)
}
*/
// MARK: - Drag Delegate
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
return parent.model.dragItems(for: indexPath)
}
// MARK: - Drop Delegate
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return parent.model.canHandle(session)
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.hasActiveDrag {
if session.items.count > 1 {
return UITableViewDropProposal(operation: .cancel)
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
} else {
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
}
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// Get last index path of table view.
let section = tableView.numberOfSections - 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: ReorderIndexPath.self) { items in
// Consume drag items.
let indexPaths = items as! [IndexPath]
for (index, sourceIndexPath) in indexPaths.enumerated() {
let destinationIndexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
self.parent.model.moveItem(at: sourceIndexPath.row, to: destinationIndexPath.row)
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
self.parent.onReorder(sourceIndexPath.row, destinationIndexPath.row)
}
}
}
}
}
And here is client code using it
struct ContentView: View {
#State private var items: [String] = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
ReorderableList(onReorder: reorder, onDelete: delete) {
ForEach(self.items, id: \.self) { item in
Text("\(item)")
}
}
.navigationBarTitle("Reorderable List", displayMode: .inline)
.navigationBarItems(trailing: Button(action: add, label: {
Image(systemName: "plus")
}))
}
}
func reorder(from source: Int, to destination: Int) {
items.move(fromOffsets: IndexSet([source]), toOffset: destination)
}
func delete(_ idx: Int) -> Bool {
items.remove(at: idx)
return true
}
func add() {
items.append("Item \(items.count)")
}
}
Problem is that it doesn't have natural refreshing behaviour of List so tapping + button in navbar and adding items does not refresh my ReorderableList
UPDATE
I have also simplified example to test this refreshing of cells as above code is a little long.
struct ReorderableList2<T, Content>: UIViewRepresentable where Content : View {
#Binding private var items: [T]
let content: (T) -> Content
init(_ items: Binding<[T]>, #ViewBuilder content: #escaping (T) -> Content) {
self.content = content
self._items = items
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "HostingCell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
uiView.reloadData()
}
class Coordinator : NSObject, UITableViewDataSource, UITableViewDelegate {
private let parent: ReorderableList2
// MARK: - Init
init(_ parent: ReorderableList2) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell
let item = parent.items[indexPath.row]
let rootView = parent.content(item)
cell.host(rootView: rootView)
return cell
}
// MARK: - Delegate
}
}
This simplified version also does not work, even when Binding items has new items added to it.
tableView:numberOfRowsInSection: is called correctly each time I am adding new item but parent.items.count is wrong old number
▿ ReorderableList2<String, Text>
▿ _items : Binding<Array<String>>
▿ transaction : Transaction
▿ plist : []
- elements : nil
▿ location : <LocationBox<ScopedLocation>: 0x6000016a4bd0>
▿ _value : 3 elements
- 0 : "Item 1"
- 1 : "Item 2"
- 2 : "Item 3"
- content : (Function)
even though in constructor or in updateUIView() checking this same items Binding gives correct updated items list.
▿ ReorderableList2<String, Text>
▿ _items : Binding<Array<String>>
▿ transaction : Transaction
▿ plist : []
- elements : nil
▿ location : <LocationBox<ScopedLocation>: 0x6000016a4bd0>
▿ _value : 5 elements
- 0 : "Item 1"
- 1 : "Item 2"
- 2 : "Item 3"
- 3 : "Item 3"
- 4 : "Item 4"
- content : (Function)
I've found such trick, but doesn't like this DispatchQueue.main.async { } mutating state in updateUIView() If someone has a better idea how to solve this problem leave other solution in comments.
As I found:
View is struct so each time its initializer is called and there are created new references to items/model properties despite them being class or struct
makeCoordinator() is called only once so when there is redrawing there is old Coordinator with old references
as we know #State is kept between view redrawings as it has some underlying storage related with it so in each redrawn View model references (different pointers) read from the same underlying storage. So updating in updateUIView() this #State refreshes this state on all referencing paths including one that Coordinator is keeping via immutable parent View reference.
import SwiftUI
extension ReorderableList2 {
struct Model<T> {
private(set) var items: [T]
init(items: [T]) {
self.items = items
}
mutating func addItem(_ item: T, at index: Int) {
items.insert(item, at: index)
}
mutating func removeItem(at index: Int) {
items.remove(at: index)
}
mutating func moveItem(at source: Int, to destination: Int) {
guard source != destination else { return }
let item = items[source]
items.remove(at: source)
items.insert(item, at: destination)
}
mutating func replaceItems(_ items: [T]) {
self.items = items
}
}
}
struct ReorderableList2: UIViewRepresentable where Content : View {
// MARK: - State
#State private(set) var model = Model<T>(items: [])
// MARK: - Properties
private let items: [T]
private let content: (T) -> Content
init(_ items: [T], #ViewBuilder content: #escaping (T) -> Content) {
self.content = content
self.items = items
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "HostingCell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
DispatchQueue.main.async {
self.model.replaceItems(self.items)
uiView.reloadData()
}
}
class Coordinator : NSObject, UITableViewDataSource, UITableViewDelegate {
private let parent: ReorderableList2
// MARK: - Init
init(_ parent: ReorderableList2) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.model.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell
let item = parent.model.items[indexPath.row]
let rootView = parent.content(item)
cell.host(rootView: rootView)
return cell
}
// MARK: - Delegate
}
}
I'm trying to create a framework that does an API call and creates a PickerView with the populated data from the API call. The iOS client will import this framework and call that one exposed function that should return the PickerView with the loaded data in it.
I managed to create a function that creates the PickerView but can't figure out how to insert the data inside the PickerView.
// Framework Side
public static func returnPickerView() -> UIPickerView {
let apple = ["apple", "orange", "durian", "banana"]
let customPicker = UIPickerView()
customPicker.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
customPicker.layer.borderColor = UIColor.black.cgColor
customPicker.layer.borderWidth = 1
return customPicker
}
Solution to this problem was to create a custom class like what JuicyFruit said.
class CustomPickerView: UIPickerView {
override init(frame: CGRect) {
super.init(frame: frame)
dataSource = self
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
extension CustomPickerView: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
...
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
...
}
}
extension CustomPickerView: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
...
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
...
}
}
I am having trouble with two of my pickerViews. I am wondering if it is possible to change the global text color of my pickerViews in the AppDelegate file?
I know some stuff can be done like:
UIPickerView.appearance().backgroundColor = color6
UIPickerView.appearance().tintColor = color4
If I need to write it in my code manually in each pickerView. What sort of pickerView should it be coded into, titleForRow?
Try to use the following class. I have subclassed the UIPickerView and specified colours inside. This is working fine. when you set the items. it will set delegate and datasource to self and will do the job for you.
import UIKit
class CustomUIPickerView: UIPickerView{
let textColor: UIColor = .white
let backGroundColor: UIColor = .blue
let _tintColor: UIColor = .white
var items: [String]?{
didSet{
self.delegate = self
self.dataSource = self
self.reloadAllComponents()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit(){
self.backgroundColor = backGroundColor
self.tintColor = _tintColor
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
}
extension CustomUIPickerView: UIPickerViewDataSource{
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.items?.count ?? 0
}
}
extension CustomUIPickerView: UIPickerViewDelegate{
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
let string = self.items?[row] ?? ""
return NSAttributedString(string: string, attributes: [NSAttributedStringKey.foregroundColor: textColor])
}
}
However if you want to handle the delegate and datasource in your view controller then you can take the advantage of using Extensions here is how:
extension String{
func stringForPickerView() -> NSAttributedString{
let color: UIColor = .white
return NSAttributedString(string: self, attributes: [NSAttributedStringKey.foregroundColor: color])
}
}
then in your ViewController:
extension ViewController: UIPickerViewDelegate{
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
return self.items[row].stringForPickerView() // from Extension
}
}
I want to select a multiple rows in pickerview in swift
this is for single row selection
`import UIKit
class ViewController: UIViewController,UIPickerViewDelegate, UIPickerViewDataSource{
#IBOutlet weak var titlelabel: UILabel!
#IBOutlet weak var pickerview: UIPickerView!
var cars = ["benz","audi","lombagini","rangerrover","bently"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
pickerview.delegate = self
pickerview.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return cars.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return cars[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
titlelabel.text = cars[row]
}
}`
did any one have any idea to select multiple rows in picker view
in objective-c AlPickerview but it is not working.
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0{
return bloodgroup.count
}
else if component == 1 {
return districk.count
}
return 1
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0{
return bloodgroup[row]
}
else if component == 1 {
return districk[row]
}
return ""
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0{
print(bloodgroup[row])
}
else if component == 1 {
print(districk[row])
}
}
I am using code from a sample project. It does work in the sample project, but not in my project.
SettingsViewController.swift:
import UIKit
class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
let days:[String] = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return days.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return days[row]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I am using Xcode 8.2.1, Swift 2.3
The method signature for numberOfComponents(in:) changed from iOS 9.3 to iOS 10. Since you are targeting a legacy version of Swift, replace your current implementation of numberOfComponents(in:) with the appropriate version below.
Swift 2.3
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
Swift 3
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
The API changes can be seen here: https://developer.apple.com/library/content/releasenotes/General/iOS10APIDiffs/Swift/UIKit.html
Modified UIPickerViewDataSource
Declaration
From
protocol UIPickerViewDataSource : NSObjectProtocol {
func numberOfComponentsInPickerView(_ pickerView: UIPickerView) -> Int
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int
}
To
protocol UIPickerViewDataSource : NSObjectProtocol {
func numberOfComponents(in pickerView: UIPickerView) -> Int
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int
}