Last active
February 7, 2024 17:36
-
-
Save damodarnamala/b6aea16c937a6cc7a26a337f11312fe6 to your computer and use it in GitHub Desktop.
Generic Tableview
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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