Skip to content

Instantly share code, notes, and snippets.

@damodarnamala
Last active February 7, 2024 17:36
Show Gist options
  • Save damodarnamala/b6aea16c937a6cc7a26a337f11312fe6 to your computer and use it in GitHub Desktop.
Save damodarnamala/b6aea16c937a6cc7a26a337f11312fe6 to your computer and use it in GitHub Desktop.
Generic Tableview
struct Post: Codable {
let id: Int
let title: String
let body: String
}
protocol PostServiceProtocol {
func fetchPosts(page: Int) -> AnyPublisher<[Post], Error>
}
class PostService: PostServiceProtocol {
func fetchPosts(page: Int) -> AnyPublisher<[Post], Error> {
let urlString = "https://jsonplaceholder.typicode.com/posts?_page=\(page)&_limit=25"
guard let url = URL(string: urlString) else {
return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [Post].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
class PostListViewModel {
@Published private(set) var posts: [Post] = []
private let postService: PostServiceProtocol
private var currentPage = 1
private var isFetching = false
private var cancellables = Set<AnyCancellable>()
init(postService: PostServiceProtocol) {
self.postService = postService
}
func fetchPosts() {
guard !isFetching else { return }
isFetching = true
postService.fetchPosts(page: currentPage)
.sink(receiveCompletion: { [weak self] completion in
switch completion {
case .failure(let error):
print("Error fetching posts: \(error.localizedDescription)")
case .finished:
self?.isFetching = false
self?.currentPage += 1
}
}, receiveValue: { [weak self] newPosts in
self?.posts.append(contentsOf: newPosts)
})
.store(in: &cancellables)
}
func fetchMorePosts() {
guard !isFetching else { return }
isFetching = true
postService.fetchPosts(page: currentPage)
.sink(receiveCompletion: { [weak self] completion in
switch completion {
case .failure(let error):
print("Error fetching more posts: \(error.localizedDescription)")
case .finished:
self?.isFetching = false
self?.currentPage += 1
}
}, receiveValue: { [weak self] newPosts in
self?.posts.append(contentsOf: newPosts)
})
.store(in: &cancellables)
}
}
class ListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var viewModel: PostListViewModel!
private var delegate: GenericTableViewDelegate<Post>!
private var cancellables = Set<AnyCancellable>()
private var bag = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setupViewModel()
setupTableView()
self.viewModel.fetchPosts()
}
private func setupViewModel() {
let postService = PostService()
viewModel = PostListViewModel(postService: postService)
}
private func setupTableView() {
self.tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
delegate = GenericTableViewDelegate<Post>(
modelsPublisher: viewModel.$posts.eraseToAnyPublisher(),
cellReuseIdentifier: "Cell",
cellConfigurator: { post, cell in
cell.textLabel?.text = post.title
},
selectionHandler: { post in
print("Selected Post: \(post.title)")
},
reloadHandler: {
self.tableView.reloadData()
},
prefetchHandler: { [weak self] in
self?.viewModel.fetchMorePosts()
}
)
tableView.dataSource = delegate
tableView.delegate = delegate
tableView.prefetchDataSource = delegate
}
}
class GenericTableViewDelegate<Model>: NSObject, UITableViewDataSource, UITableViewDelegate, UITableViewDataSourcePrefetching {
typealias CellConfigurator = (Model, UITableViewCell) -> Void
typealias SelectionHandler = (Model) -> Void
typealias PrefetchHandler = () -> Void
typealias ReloadHandler = () -> Void
private var models: [Model] = []
private let cellReuseIdentifier: String
private let cellConfigurator: CellConfigurator
private let selectionHandler: SelectionHandler
private let reloadHandler: ReloadHandler
private let prefetchHandler: PrefetchHandler?
private var cancellables = Set<AnyCancellable>()
init(modelsPublisher: AnyPublisher<[Model], Never>,
cellReuseIdentifier: String,
cellConfigurator: @escaping CellConfigurator,
selectionHandler: @escaping SelectionHandler,
reloadHandler: @escaping ReloadHandler,
prefetchHandler: PrefetchHandler? = nil) {
self.cellReuseIdentifier = cellReuseIdentifier
self.cellConfigurator = cellConfigurator
self.selectionHandler = selectionHandler
self.prefetchHandler = prefetchHandler
self.reloadHandler = reloadHandler
super.init()
modelsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] newModels in
self?.models.append(contentsOf: newModels)
self?.reloadHandler()
}
.store(in: &cancellables)
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
let model = models[indexPath.row]
cellConfigurator(model, cell)
return cell
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let model = models[indexPath.row]
selectionHandler(model)
}
// MARK: - UITableViewDataSourcePrefetching
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
prefetchHandler?()
}
func reset() {
self.models = []
}
}
class Cell: UITableViewCell {
override var reuseIdentifier: String? {
return "Cell"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment