I have a UIViewControllerRepresentable wrapper for UITableViewController and am using swift composable architecture, which is probably irrelevant to the issue.
Here's my table view wrapper code, including the context menu code (I have omitted quite a lot of setup code):
public struct List<EachState, EachAction, RowContent, RowPreview, Destination, Data, ID>: UIViewControllerRepresentable, KeyPathUpdateable
where Data: Collection, RowContent: View, RowPreview: View, Destination: View, EachState: Identifiable, EachState.ID == ID {
private var actionProvider: (IndexSet) -> UIMenu? = { _ in nil }
private var previewProvider: (Store<EachState, EachAction>) -> RowPreview? = { _ in nil }
// setup code
public func makeUIViewController(context: Context) -> UITableViewController {
let tableViewController = UITableViewController()
tableViewController.tableView.translatesAutoresizingMaskIntoConstraints = false
tableViewController.tableView.dataSource = context.coordinator
tableViewController.tableView.delegate = context.coordinator
tableViewController.tableView.separatorStyle = .none
tableViewController.tableView.register(HostingCell<RowContent>.self, forCellReuseIdentifier: "Cell")
return tableViewController
}
public func updateUIViewController(_ controller: UITableViewController, context: Context) {
context.coordinator.rows = data.enumerated().map { offset, item in
store.scope(state: { $0[safe: offset] ?? item },
action: { (item.id, $0) })
}
controller.tableView.reloadData()
}
public func makeCoordinator() -> Coordinator {
Coordinator(rows: [],
content: content,
onDelete: onDelete,
actionProvider: actionProvider,
previewProvider: previewProvider,
destination: destination)
}
public func previewProvider(_ provider: #escaping (Store<EachState, EachAction>) -> RowPreview?) -> Self {
update(\.previewProvider, value: provider)
}
public func destination(_ provider: #escaping (Store<EachState, EachAction>) -> Destination?) -> Self {
update(\.destination, value: provider)
}
public class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
fileprivate var rows: [Store<EachState, EachAction>]
private var content: (Store<EachState, EachAction>) -> RowContent
private var actionProvider: (IndexSet) -> UIMenu?
private var previewProvider: (Store<EachState, EachAction>) -> RowPreview?
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? HostingCell<RowContent>,
let view = rows[safe: indexPath.row] else {
return UITableViewCell()
}
tableViewCell.setup(with: content(view))
return tableViewCell
}
public func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
onDelete(IndexSet(integer: indexPath.item))
}
}
public func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let store = rows[safe: indexPath.row] else { return nil }
return UIContextMenuConfiguration(
identifier: nil,
previewProvider: {
guard let preview = self.previewProvider(store) else { return nil }
let hosting = UIHostingController<RowPreview>(rootView: preview)
return hosting
},
actionProvider: { _ in
self.actionProvider(IndexSet(integer: indexPath.item))
})
}
}
}
private class HostingCell<Content: View>: UITableViewCell {
var host: UIHostingController<Content>?
func setup(with view: Content) {
if host == nil {
let controller = UIHostingController(rootView: view)
host = controller
guard let content = controller.view else { return }
content.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(content)
content.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
content.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
content.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
} else {
host?.rootView = view
}
setNeedsLayout()
}
}
And here's an example usage:
private struct ClassView: View {
let store = Store<ClassState, ClassAction>(
initialState: ClassState(),
reducer: classReducer,
environment: ClassEnv()
)
var body: some View {
WithViewStore(store) { viewStore in
CoreInterface.List(store.scope(state: \.people, action: ClassAction.personAction)) { store in
PersonView(store: store)
}
.actionProvider { indices in
let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in
viewStore.send(.remove(indices))
}
return UIMenu(title: "", children: [delete])
}
.previewProvider { viewStore in
Text("preview")
}
}
}
}
The issue is as follows: when I long tap on a cell to show the context menu, then dismiss it and scroll up, the table view disappears. This only happens when it's inside a NavigationView. Here is a short video of the issue.
The project is on github. The table view wrapper is in InternalFrameworks/Core/CoreInterface/Views/List, usage is in InternalFrameworks/Screens/QuickWorkoutsList/Source/QuickWorkoutsList. In order to run the project, you'll need xcodegen. Run
brew install xcodegen
xcodegen generate
Related
I have a UICollectionView placed inside a UITableViewCell. The collection view has its scroll direction set to horizontal. I have set the collection view and the collection view cell in the table view right, but when I run it the images don't show up and as I see that data doesn't pass right from table view cell to collection view cell. I cannot find anything wrong. I hope someone can find the mistake with a clearer mind.
Imagine that I want the first cell to have a collection view of images horizontally and in the other cells rows of names for example.
You can see my project on this GitHub account: https://github.com/BenSeferidis/Nft-Assets/tree/v5 for better understanding.
Lobby View Controller (Main VC):
The AssetTableViewCell is another custom cell that generates the rest of the data from my models: NFT API
import UIKit
class LobbyViewController: UIViewController {
// MARK: - IBProperties
#IBOutlet weak var tableView: UITableView!
// MARK: - Properties
var data: [DataEnum] = []
var likes:[Int] = []
var numlikes: Int = 0
var nfts: [Nft] = []
let creators : [Creator] = []
var icons: [Icon] = []
var loadData = APICaller()
// MARK: - Life Cyrcle
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "AssetTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "AssetTableViewCell")
let nib2 = UINib(nibName: "CreatorsTableViewCell", bundle: nil)
tableView.register(nib2, forCellReuseIdentifier: "CreatorsTableViewCell")
tableView.dataSource = self //method to generate cells,header and footer before they are displaying
tableView.delegate = self //method to provide information about these cells, header and footer ....
downloadJSON {
self.tableView.reloadData()
print("success")
}
loadData.downloadData { (result) in
print(result)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? PresentViewController {
destination.nft = nfts[tableView.indexPathForSelectedRow!.row]
destination.delegate = self
}
}
// MARK: - Methods
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https://public.arx.net/~chris2/nfts.json")
URLSession.shared.dataTask(with: url!) { [self] data, response, error in
if error == nil {
do {
self.nfts = try JSONDecoder().decode([Nft].self, from: data!)
let creators = nfts.map { nft in
nft.creator
}
self.data.append(.type1(creators: creators))
self.nfts.forEach { nft in
self.data.append(.type2(nft: nft))
}
DispatchQueue.main.async {
completed()
}
}
catch {
print("error fetching data from api")
}
}
}.resume()
}
}
// MARK: - Extensions
extension LobbyViewController : UITableViewDelegate , UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? 200 : UITableView.automaticDimension
}
//gemizo ta rows tou table
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch self.data[indexPath.item] {
case .type1(let creators):
print("--->", creators)
let cell = tableView.dequeueReusableCell(withIdentifier: "CreatorsTableViewCell",
for: indexPath) as! CreatorsTableViewCell
cell.updateCreators(creators)
return cell
case .type2(let nft):
let cell = tableView.dequeueReusableCell(withIdentifier: "AssetTableViewCell",
for: indexPath) as! AssetTableViewCell
cell.nameLabel?.text = nft.name
cell.nameLabel.layer.cornerRadius = cell.nameLabel.frame.height/2
cell.likesLabel?.text = "\((numlikes))"
let imgUrl = (nft.image_url)
print(imgUrl)
cell.iconView.downloaded(from: imgUrl)
cell.iconView.layer.cornerRadius = cell.iconView.frame.height/2
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetails", sender: self)
}
}
extension LobbyViewController : TestDelegate{
func sendBackTheLikess(int: Int) {
numlikes = int
tableView.reloadData()
}
}
// MARK: - Enums
enum DataEnum {
case type1(creators: [Creator])
case type2(nft: Nft)
}
// MARK: - Struct
struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
Creators TableView Cell :
import UIKit
class CreatorsTableViewCell: UITableViewCell {
//MARK: - IBProtperties
#IBOutlet var creatorsCollectionView: UICollectionView!
//MARK: - Properties
var nft : Nft?
var creators : [Creator] = []
weak var delegate : CreatorsTableViewCellDelegate?
//MARK: - Life Cyrcle
override func awakeFromNib() {
super.awakeFromNib()
creatorsCollectionView.dataSource = self
creatorsCollectionView.delegate = self
let nibName = UINib(nibName: "CollectionViewCell", bundle: nil)
creatorsCollectionView.register(nibName, forCellWithReuseIdentifier: "CollectionViewCell")
}
func updateCreators( _ creators: [Creator]) {
self.creators = creators
}
required init?(coder aDecoder : NSCoder) {
super.init(coder: aDecoder)
}
}
//MARK: - Extensions
extension CreatorsTableViewCell : UICollectionViewDelegate , UICollectionViewDataSource , UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: 30, height: 30)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return creators.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = creatorsCollectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell",
for: indexPath) as! CollectionViewCell
cell.renewCreators(creators)
cell.creatorName.text = creators[indexPath.row].user.username
cell.creatorName.layer.cornerRadius = cell.creatorName.frame.height/2
cell.creatorsImg.image = UIImage(named: creators[indexPath.row].profileImgURL )
cell.creatorsImg.layer.cornerRadius = cell.creatorsImg.frame.height/2
return cell
// cell.backgroundColor = .brown
// cell.creatorName?.text = creators[indexPath.row].user.username
// let imgUrl = (creators[indexPath.row].profileImgURL)
// print(imgUrl)
// cell.creatorsImg.downloaded(from: imgUrl)
// return cell
}
}
//MARK: - Protocols
protocol CreatorsTableViewCellDelegate: AnyObject {
func didSelectPhoto(index: Int)
}
CollectionViewCell:
import UIKit
class CollectionViewCell: UICollectionViewCell {
//MARK: - IBProperties
#IBOutlet var creatorsImg: UIImageView!{
didSet {
creatorsImg.contentMode = .scaleAspectFit
}
}
#IBOutlet var creatorName: UILabel!
//MARK: - Properties
var nft : Nft?
var creators : [Creator] = []
//MARK: - Life Cyrcle
override func awakeFromNib() {
super.awakeFromNib()
print(creators)
creatorName.backgroundColor = .systemCyan
creatorsImg.layoutIfNeeded()
creatorsImg.layer.cornerRadius = creatorsImg.frame.height / 2
}
func setUpCollectionViewCell(_ nft: Nft) {
}
func renewCreators( _ creators: [Creator]) {
self.creators = creators
}
}
//MARK: - Protocols
protocol CollectionViewCellDelegate: AnyObject {
func didSelectPhoto(index: Int)
}
Models:
import Foundation
// MARK: - Nft
struct Nft: Codable{
let id:Int
let image_url:String
let name:String
let creator: Creator
}
// MARK: - Icon
struct Icon:Codable{
let image_url:String
}
// MARK: - Creator
struct Creator: Codable {
let user: User
let profileImgURL: String
enum CodingKeys: String, CodingKey {
case user
case profileImgURL = "profile_img_url"
}
}
// MARK: - User
struct User: Codable {
let username: String?
}
APICaller :
import Foundation
final class APICaller {
static let shared = APICaller()
public struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
public func downloadData(completion:#escaping (Result<[Nft], Error>) -> Void )
{
guard let url = URL(string:Constants.url)else{
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
//print(response)
print("here")
guard let data = data , error == nil else{
print("something went wrong")
return
}
print("here4")
//mexri edo exoume parei ta data kai tora me to do-catch tha ta kanoume convert se object
do{
//Decode the response
let nfts = try JSONDecoder().decode([Nft].self, from: data)
completion(.success(nfts))
print(nfts)
}catch{
completion(.failure(error))
}
}
task.resume()
}
}
I am writing an application to display news from the server. Created a StartController class that only displays data from the ViewModel. There are no warnings in the project, but at startup, a blank screen.
final class StartController: UIViewController {
// MARK: - Private properties
private var viewModel: ViewModel?
private let identifier = "tapeCell"
private let tableView = CustomTableView(frame: .zero)
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
setUpConstraints()
reloadViewModel()
}
// MARK: - Private functions
private func reloadViewModel() {
viewModel?.configData { [weak self] in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
}
private func setUpView() {
view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: identifier)
view.addSubview(tableView)
}
}
// MARK: - Delegate
extension StartController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
// MARK: - DataSource
extension StartController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel?.numberOfRowsInSection() ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
let VM = self.viewModel?.articleAtIndex(indexPath.row)
var content = cell.defaultContentConfiguration()
content.text = VM?.title
content.secondaryText = VM?.author
cell.contentConfiguration = content
return cell
}
}
All the logic takes place inside the ViewModel, downloading data from the network and transferring it to the StartController. But apparently, no data is being transmitted.
final class ViewModel {
// MARK: - Private properties
private let service = TapeService()
private var data: [Articles] = []
// MARK: - Functions
func configData(completion: #escaping() -> ()) {
service.tapeAdd { [weak self] result in
switch result {
case .success(let articles):
self?.data = articles
case .failure(let error):
print(error.localizedDescription)
}
}
}
func numberOfRowsInSection() -> Int {
return data.count
}
func articleAtIndex(_ index: Int) -> ArticleViewModel {
let article = self.data[index]
return ArticleViewModel(article)
}
}
// MARK: - ArticleViewModel
final class ArticleViewModel {
private let article: Articles
init(_ article: Articles) {
self.article = article
}
var title: String {
return self.article.title ?? " "
}
var author: String {
return self.article.author ?? " "
}
}
I would be very grateful if you could tell me what my mistake is and what needs to be done to display the data. Thanks!
I have problem with use Button as content in UITableView(UIViewRepresentable), which contains in Sheet. Button stayed pressed, when you try scroll table.
Here toy can download my proj https://github.com/MaksimBezdrobnoi/UITableViewInSheet
I create a simple UITableView where put SUI Button, and put Table in SUI Sheet.
Here my SUI view
struct TableSample: View {
var body: some View {
ZStack {
Color.clear
.sheet(isPresented: .constant(true)) {
AwesomeNewTable {
Button(action: {
print("HELLO")
}, label: {
Color.red
.frame(height: 30)
.padding(.horizontal, 16)
.padding(.bottom, 4)
})
}
}
}
}
}
And here my TableView
struct AwesomeNewTable<Content: View>: UIViewRepresentable {
private let content: () -> Content
init(content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.separatorStyle = .none
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingCell<Content>.self, forCellReuseIdentifier: "Cell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
context.coordinator.parent = self
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
var parent: AwesomeNewTable
init(parent: AwesomeNewTable) {
self.parent = parent
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
150
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? HostingCell<Content> else {
return UITableViewCell()
}
let view = parent.content()
tableViewCell.setup(with: view)
return tableViewCell
}
}
}
I have implemented pagination in UITableView with WillDisplay method. Pagination process is working fine but if I need to reload a list on button click, then data is appending in the list. How to work around with this ?
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if (indexPath.row + 1 == playlistViewModel.numberOfRowsInSection()) {
if playlistViewModel.isReload != false {
pageIncrement += 1
playlistViewModel.playListingApi(enterView: false, page: pageIncrement)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
pageIncrement = 1
playlistViewModel.playListingApi(enterView: true, page: pageIncrement)
}
playlistViewModel.hitNextApiClosure = { [weak self] () in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self?.playlistViewModel.isReload = false
self?.playlistViewModel.playlistArray?.removeAll()
self?.playlistTableView.reloadData()
self?.pageIncrement = 1
self?.playlistViewModel.playListingApi(enterView: true, page: self?.pageIncrement ?? 1)
}
}
And ViewModel method is
func playListingApi(enterView: Bool, page: Int) {
self.isLoading = true
if (enterView){
playlistArray?.removeAll()
isReload = false
}
playlistService.getPlayList(page: "\(page)", limit: "20") { (result) in
self.isLoading = false
switch result {
case .success(let data):
self.playlist = data as? Playlist
guard let data = self.playlist?.data?.blocks else {
self.errorMessage = AlertMessage.somethingWentWrong
return
}
for playlistData in data {
self.playlistArray?.append(playlistData)
self.isReload = true
}
if (data.count == 0){
self.isReload = false
}
self.reloadTableBool = true
case .error(let message):
self.isReload = false
self.errorMessage = message
}
}
}
When you are reloading your tableView set page = 1 , empty tableView data source and reload tableView. Finally hitAPI for fresh set of data .
page = 1
mTblDataSource.removeAll()
mTableView.reloadData()
hitAPI()
Consider this one as a possible solution.
public class Pageable<T> {
public enum ObjectState {
case loading
case loaded
}
public private (set) var page: Int = 0
private var items: [T] = [T]()
private var state: ObjectState = .loading
private let itemsPerPage: Int
private var itemsReloaded: (() -> ())
public init(itemsPerPage: Int, items: [T] = [], itemsReloaded: #escaping (() -> ())) {
self.items = items
self.itemsPerPage = itemsPerPage
self.itemsReloaded = itemsReloaded
}
public var itemsCount: Int {
switch state {
case .loaded:
return items.count
case .loading:
return items.count + 1 // should be displaying cell with loading indicator
}
}
public var isLoaded: Bool {
return state == .loaded
}
public var isLoading: Bool {
return state == .loading
}
public func append(contentsOf items: [T]) {
state = items.count < itemsPerPage ? .loaded : .loading
self.items.append(contentsOf: items)
itemsReloaded()
}
public func incrementPage() {
page += 1
}
public func reset() {
page = 0
state = .loading
items = []
}
public func itemFor(_ index: Int) -> T? {
return items.indices.contains(index) ? items[index] : nil
}
}
struct Property {}
protocol SearchItemsDisplayLogic: class {
func reloadItemsViews()
}
protocol SearchItemsInteraction {
func loadMore(page: Int)
}
// MARK: View Related with UITableView example
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(pullToRefresh(_:)), for: UIControl.Event.valueChanged)
return refreshControl
}()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter.itemsCount
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
presenter.viewWillDisplayCellAt(indexPath.row)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if presenter.isLoadingCellNeeded(indexPath.row) {
return tableView.dequeueReusableCell(withIdentifier: "\(LoadingTableViewCell.self)", for: indexPath)
}
let cell = tableView.dequeueReusableCell(withIdentifier: "\(PropertyTableViewCell.self)", for: indexPath) as? PropertyTableViewCell
presenter.populate(cell: cell, indexPath: indexPath)
return cell ?? UITableViewCell(style: .default, reuseIdentifier: "\(UITableViewCell.self)")
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let property = presenter.property(indexPath.row) else {
return
}
}
protocol SearchItemsPresentation {
// MARK: Pagination logic
var itemsCount: Int { get }
// From the view.
func isLoadingCellNeeded(_ item: Int) -> Bool
func viewWillDisplayCellAt(_ item: Int)
func pullToRefresh()
func property(_ item: Int) -> Property?
// From the interactor.
func presentItems(items: [Property])
}
// MARK: - Presenter
class SearchItemsPresenter: SearchItemsPresentation {
weak var propertyDisplay: SearchItemsDisplayLogic?
lazy var interactor: SearchItemsInteraction? = {
return SearchItemsInteractor(presenter: self)
}()
var itemsCount: Int {
return pageable.itemsCount
}
private var pageable: Pageable<Property>!
init(viewController: SearchItemsDisplayLogic) {
self.propertyDisplay = viewController
pageable = Pageable(itemsPerPage: 15, itemsReloaded: {
self.propertyDisplay?.reloadItemsViews()
})
}
// TODO: presenter should not have UIKit!
func populate(cell: CellProtocol?, indexPath: IndexPath) {
guard let cell = cell else { return }
// populate
}
}
extension SearchItemsPresenter {
func property(_ index: Int) -> Property? {
return pageable.itemFor(index)
}
}
// MARK: Pageable
extension SearchItemsPresenter {
/// if it's loading show loading cell in the view.
func isLoadingCellNeeded(_ item: Int) -> Bool {
let isViewAtTheBottom = item == itemsCount - 1
return isViewAtTheBottom && pageable.isLoading
}
/// Called in `willDisplay` methods of the view.
func viewWillDisplayCellAt(_ item: Int) {
let isViewAtTheBottom = item == itemsCount - 1
if isViewAtTheBottom && pageable.isLoading {
interactor?.loadMore(page: pageable.page)
pageable.incrementPage()
}
}
func pullToRefresh() {
pageable.reset()
interactor?.loadMore(page: pageable.page)
pageable.incrementPage()
}
func presentItems(items: [Property]) {
pageable.append(contentsOf: items)
}
}
// MARK: - Interactor
class SearchItemsInteractor: SearchItemsInteraction {
private var presenter: SearchItemsPresentation
init(presenter: SearchItemsPresentation) {
self.presenter = presenter
}
func loadMore(page: Int) {
DispatchQueue.global(qos: .background).async {
sleep(1)
DispatchQueue.main.async {
// TODO: return some data
self.presenter.presentItems(items: [])
}
}
}
}
I have two Lists each with simple items. I can rearrange items within a list, with the code shown below. I want to be able to drag an item from one list and drop it into the other list. Not sure what I need to enable to make that happen. As is, I can drag something from one list all over the screen, but I can't drop it anywhere except within its own list; if I release the drag anywhere else, it just flies back to its original location.
Clearly, I need to add something(s) to enable the desired behavior; any ideas on what's required (not necessarily using .onMove -- that's within the same list) would be most appreciated.
struct ContentView: View {
#State private var users1 = ["AAA", "BBB", "CCC"]
#State private var users2 = ["DDD", "EEE", "FFF"]
#State private var isEditable = true
var body: some View {
HStack {
Spacer()
List {
ForEach(users1, id: \.self) { user in
Text(user)
.background(Color(.yellow))
}
.onMove(perform: move1)
}
.environment(\.editMode, isEditable ? .constant(.active) : .constant(.inactive))
Spacer()
List {
ForEach(users2, id: \.self) { user in
Text(user)
.background(Color(.orange))
}
.onMove(perform: move2)
}
.environment(\.editMode, isEditable ? .constant(.active) : .constant(.inactive))
Spacer()
}
}
func move1(from source: IndexSet, to destination: Int) {
users1.move(fromOffsets: source, toOffset: destination)
}
func move2(from source: IndexSet, to destination: Int) {
users2.move(fromOffsets: source, toOffset: destination)
}
}
I don't think this is possible with SwiftUI yet. This piece of code shows how you use two UITableViews. I hope you can improve this to achieve what you are looking for.
First create this class which does all the magic. You need to import UIKit and MobileCoreServices.
class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate {
var leftTableView = UITableView()
var rightTableView = UITableView()
var removeIndex = IndexPath()
var leftItems: [String] = [
"Hello",
"What",
"is",
"happening"
]
var rightItems: [String] = [
"I",
"don't",
"know"
]
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let string = tableView == leftTableView ? leftItems[indexPath.row] : rightItems[indexPath.row]
self.removeIndex = indexPath
guard let data = string.data(using: .utf8) else { return [] }
let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)
return [UIDragItem(itemProvider: itemProvider)]
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let section = tableView.numberOfSections - 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: NSString.self) { items in
guard let strings = items as? [String] else {
return
}
var indexPaths = [IndexPath]()
for (index, string) in strings.enumerated() {
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
if tableView == self.leftTableView {
self.leftItems.insert(string, at: indexPath.row)
} else {
self.rightItems.insert(string, at: indexPath.row)
}
indexPaths.append(indexPath)
}
if tableView == self.leftTableView {
self.rightItems.remove(at: self.removeIndex.row)
self.rightTableView.deleteRows(at: [self.removeIndex], with: .automatic)
} else {
self.leftItems.remove(at: self.removeIndex.row)
self.leftTableView.deleteRows(at: [self.removeIndex], with: .automatic)
}
tableView.insertRows(at: indexPaths, with: .automatic)
}
}
override func viewDidLoad() {
super.viewDidLoad()
leftTableView.dataSource = self
rightTableView.dataSource = self
leftTableView.frame = CGRect(x: 0, y: 40, width: 150, height: 400)
rightTableView.frame = CGRect(x: 150, y: 40, width: 150, height: 400)
leftTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
rightTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(leftTableView)
view.addSubview(rightTableView)
leftTableView.dragDelegate = self
leftTableView.dropDelegate = self
rightTableView.dragDelegate = self
rightTableView.dropDelegate = self
leftTableView.dragInteractionEnabled = true
rightTableView.dragInteractionEnabled = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == leftTableView {
return leftItems.count
} else {
return rightItems.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if tableView == leftTableView {
cell.textLabel?.text = leftItems[indexPath.row]
} else {
cell.textLabel?.text = rightItems[indexPath.row]
}
return cell
}
}
After that you wrap this:
struct TableView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> TableViewController {
let v = TableViewController()
return v
}
func updateUIViewController(_ viewController: TableViewController, context: Context) {
}
}
and finally use it:
struct ContentView: View {
var body: some View {
VStack {
TableView()
}
}
}
I hope this helps.