SwiftUI List initializers how it is implemented? - list

I can do something like this to resemble my CustomList to native SwiftUI List
But I think this approach is not very efficient and the way SwiftUI does it underneath its List component.
// MARK: - Properties
private let items: [AnyView]
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 {
var items = [AnyView]()
data.forEach { element in
let item = rowContent(element)
items.append(AnyView(item))
}
self.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 {
var items = [AnyView]()
data.forEach { element in
let item = rowContent(element)
items.append(AnyView(item))
}
self.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 {
var items = [AnyView]()
data.forEach { element in
let item = rowContent(element)
items.append(AnyView(item))
}
self.items = items
}
How I can make this all this init store something like this in properties
private let items: Data // where Data : RandomAccessCollection
private let rowContent: (Data.Element) -> Content
and make all init Data including RandomAccessCollection conform to this
I have tries many ways to achieve this but doesn't know how to do this.
I want to generate view like this
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.rowContent(item)
cell.host(rootView: rootView)
return cell
}

Related

Using SwiftUI View for UICollectionViewCell

I made UICollectionView that accept (Item) -> Content (where Content:View) as init parameter to pass this SwiftUI View to cell content view using UIHostController. But by some reason my screen is empty, however if I pass some View instead of Content directly to UIHosting controller everything works fine.
Code:
Cell
final class SnapCarouselCell<Content:View>: UICollectionViewCell{
var cellContent: Content?
override init(frame: CGRect) {
super.init(frame: frame)
let vc = UIHostingController(rootView: cellContent)
contentView.addSubview(vc.view)
vc.view.translatesAutoresizingMaskIntoConstraints = false
vc.view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
vc.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
vc.view.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
vc.view.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Controller
class SnapCarouselViewController<Item: Equatable, Content: View>: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
lazy var snapCarouselView: UICollectionView = {
let collectionView = setUpCollectionView()
self.view.addSubview(collectionView)
return collectionView
}()
private let flowLayout: UICollectionViewFlowLayout
private let cellHeight: CGFloat
private let cellWidth: CGFloat
private var centerCell: UICollectionViewCell?
private let items: [Item]
private let cellContent: (Item) -> Content
init(
cellHeight: CGFloat,
cellWidth: CGFloat,
items: [Item],
#ViewBuilder cellContent: #escaping (Item) -> Content
){
self.cellHeight = cellHeight
self.cellWidth = cellWidth
self.flowLayout = SnapCarouselViewFlowLayout(cellWidth: cellWidth, cellHeight: cellHeight)
self.items = items
self.cellContent = cellContent
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
snapCarouselView.dataSource = self
snapCarouselView.delegate = self
let indexPath = IndexPath(row: 9999999999, section: 0)
snapCarouselView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Int.max
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SnapCarouselCell", for: indexPath) as! SnapCarouselCell<Content>
cell.cellContent = cellContent(items[indexPath.row % items.count])
return cell
}
private func setUpCollectionView() -> UICollectionView{
let collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: flowLayout)
collectionView.backgroundColor = .white
collectionView.showsHorizontalScrollIndicator = false
collectionView.frame = CGRect(x: 0, y: 100, width: view.frame.size.width, height: cellHeight)
collectionView.decelerationRate = .fast
collectionView.register(SnapCarouselCell<Content>.self, forCellWithReuseIdentifier: "SnapCarouselCell")
return collectionView
}
}
View
struct SnapCarouselView<Content: View, Item: Equatable>: UIViewControllerRepresentable {
private let cellContent: (Item) -> Content
private let cellHeight: CGFloat
private let cellWidth: CGFloat
private let items: [Item]
init(
cellHeight: CGFloat,
cellWidth: CGFloat,
items: [Item],
#ViewBuilder cellContent: #escaping (Item) -> Content
) {
self.cellHeight = cellHeight
self.cellWidth = cellWidth
self.cellContent = cellContent
self.items = items
}
func makeUIViewController(context: Context) -> SnapCarouselViewController<Item,Content> {
let vc = SnapCarouselViewController(
cellHeight: cellHeight,
cellWidth: cellWidth,
items: items,
cellContent: cellContent)
return vc
}
func updateUIViewController(_ uiViewController: SnapCarouselViewController<Item,Content>, context: Context) {
}
typealias UIViewControllerType = SnapCarouselViewController
}
struct TestUICollectionView_Previews: PreviewProvider {
static var previews: some View {
SnapCarouselView(cellHeight: 200, cellWidth: 200, items: test) { item in
Text(item.name)
}
}
}
struct Test: Equatable{
var id = UUID()
let name : String
}
let test = [
Test(name: "1"),
Test(name: "2"),
Test(name: "3"),
Test(name: "4"),
Test(name: "5"),
Test(name: "6"),
Test(name: "7"),
Test(name: "8"),
Test(name: "9"),
Test(name: "10"),
Test(name: "12"),
Test(name: "12"),
Test(name: "13"),
Test(name: "14"),
Test(name: "15"),
]

NSTableView with scroll view and header view in SwiftUI

I'm trying to implement an NSTableView in a SwiftUI app in MacOS using NSViewRepresentable. The part of creating the table view works well so far:
struct TableView: NSViewRepresentable {
typealias NSViewType = NSTableView
func makeNSView(context: Context) -> NSTableView {
let tableView = NSTableView(frame: .zero)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.rowHeight = 20.0
tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Name"))
nameColumn.title = "Name"
tableView.addTableColumn(nameColumn)
let ageColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Age"))
ageColumn.title = "Age"
tableView.addTableColumn(ageColumn)
return tableView
}
func updateNSView(_ nsView: NSTableView, context: Context) {
nsView.reloadData()
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}
extension TableView {
final class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return 12
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
switch tableColumn?.identifier.rawValue {
case "Name":
return "Person \(row)"
case "Age":
return "\(25 + row)"
default:
return nil
}
}
func tableView(_ tableView: NSTableView, shouldEdit tableColumn: NSTableColumn?, row: Int) -> Bool {
return true
}
func tableView(_ tableView: NSTableView, setObjectValue object: Any?, for tableColumn: NSTableColumn?, row: Int) {
}
}
}
Of course, this only gives me the pure table, without a NSScrollView or a NSHeaderView.
My question is now: which would be the most elegant and SwiftUI-ish way to integrate NSScrollView and NSHeaderView?
bundle all three with NSViewRepresentable (very monolithic)
pull the NSHeaderView from my NSViewRepresentable and somehow set it up inside SwiftUI's ScrollView (not sure how I would achieve that it only scrolls horizontally though)
something else?
Thanks!
Thanks for you question, I used your code as base for my solution.
Basically to show the NSTable header you need to wrap the NSTableView in a ScrollView.
This is my final code
struct TableView: NSViewRepresentable {
typealias NSViewType = NSScrollView
func makeNSView(context: Context) -> NSScrollView {
let tableContainer = NSScrollView(frame: .zero)
let tableView = NSTableView(frame: .zero)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.rowHeight = 20.0
tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Name"))
nameColumn.title = "Name"
tableView.addTableColumn(nameColumn)
let ageColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Age"))
ageColumn.title = "Age"
tableView.addTableColumn(ageColumn)
tableContainer.documentView = tableView
tableContainer.hasVerticalScroller = true
return tableContainer
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
guard let tableView = nsView.documentView as? NSTableView else { return }
tableView.reloadData()
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}

SwiftUI SearchBar problem with NavigationLink

I've a problem in SwiftUI with the searchBar appear.
There's a delay on its appear when I use NavigationLink. I saw that the problem appears only with NavigationLinks, if I use a conditional overlay or others "handmade" way to move between Views the problem doesn't appear. You know what I could do to fix the problem?
Here's my views code:
import SwiftUI
struct ContentView: View {
#State var searchText = ""
var body: some View {
NavigationView{
NavigationLink(destination: ContentView2()){
Text("Go to Sub View")
}
.navigationBarTitle("Main View")
.add(SearchBar(text: self.$searchText, hide: true, placeholder: "Search", cancelButton: true, autocapitalization: .sentences))
}
}
}
struct ContentView2 : View {
#State var searchText = ""
var body: some View {
Text("Hello, world!")
.navigationBarTitle("Sub View")
.add(SearchBar(text: self.$searchText, hide: true, placeholder: "Search", cancelButton: true, autocapitalization: .sentences))
}
}
My SearchBar code
import SwiftUI
class SearchBar: NSObject, ObservableObject {
let searchController: UISearchController = UISearchController(searchResultsController: nil)
#Binding var text: String
let hide : Bool
let placeholder : String
let cancelButton : Bool
let autocapitalization : UITextAutocapitalizationType
init(text: Binding<String>, hide: Bool, placeholder: String, cancelButton: Bool, autocapitalization: UITextAutocapitalizationType) {
self._text = text
self.hide = hide
self.placeholder = placeholder
self.cancelButton = cancelButton
self.autocapitalization = autocapitalization
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
self.searchController.hidesNavigationBarDuringPresentation = hide
self.searchController.automaticallyShowsCancelButton = cancelButton
self.searchController.searchBar.placeholder = placeholder
self.searchController.searchBar.autocapitalizationType = autocapitalization
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
My ViewController code
import SwiftUI
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
override func viewDidAppear(_ animated: Bool) {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
self.parent?.definesPresentationContext = true
self.parent?.navigationController?.navigationBar.sizeToFit()
}
override func viewDidDisappear(_ animated: Bool) {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
self.parent?.definesPresentationContext = true
self.parent?.navigationController?.navigationBar.sizeToFit()
}
}
And here's a video of the problem
Set the hidesSearchBarWhenScrolling property before the SearchBar is displayed on the screen. This can be done in viewWillAppear or as in the example below:
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}

SwiftUI: Implementing wrapper around UITableView to achieve custom List-like view

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
}
}

change global textcolor of UIPickerView possible?

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
}
}