IGListKit/Examples/Examples-iOS/IGListKitExamples/ViewControllers/FeedViewController.swift

212 lines
6.5 KiB
Swift
Raw Normal View History

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import IGListKit
import UIKit
final class FeedViewController: UIViewController, ListAdapterDataSource {
// MARK: - Properties
private lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self)
}()
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .systemBackground
collectionView.alwaysBounceVertical = true
return collectionView
}()
private var posts: [Post] = []
private var isLoading = false
private var shouldShowLoadingCell = false
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupAdapter()
loadInitialData()
}
// MARK: - Setup
private func setupUI() {
title = "Feed View"
view.backgroundColor = .systemBackground
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .refresh,
target: self,
action: #selector(refreshFeed)
)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func setupAdapter() {
adapter.collectionView = collectionView
adapter.scrollViewDelegate = self
adapter.dataSource = self
}
// MARK: - Data Loading
private func loadInitialData() {
isLoading = true
APIService.shared.resetPagination()
posts = []
shouldShowLoadingCell = true
adapter.performUpdates(animated: true)
APIService.shared.fetchPosts { [weak self] newPosts in
guard let self = self else { return }
self.posts = newPosts
self.isLoading = false
self.adapter.performUpdates(animated: true)
}
}
private func loadMoreData() {
guard !isLoading else { return }
isLoading = true
shouldShowLoadingCell = true
adapter.performUpdates(animated: true)
APIService.shared.fetchPosts { [weak self] newPosts in
guard let self = self else { return }
// Append new posts to existing posts
self.posts.append(contentsOf: newPosts)
self.isLoading = false
// If no new posts were fetched, hide the loading cell
if newPosts.isEmpty {
self.shouldShowLoadingCell = false
}
self.adapter.performUpdates(animated: true)
}
}
@objc private func refreshFeed() {
loadInitialData()
}
// MARK: - ListAdapterDataSource
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
var objects: [ListDiffable] = posts
if shouldShowLoadingCell {
objects.append(LoadingCellModel())
}
return objects
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
if object is Post {
let sectionController = PostSectionController()
sectionController.delegate = self
return sectionController
} else {
return LoadingSectionController()
}
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
let emptyView = UIView()
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "No posts available"
label.textAlignment = .center
label.textColor = .gray
emptyView.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor)
])
return emptyView
}
}
// MARK: - UIScrollViewDelegate
extension FeedViewController: UIScrollViewDelegate {
func scrollViewWillEndDragging(_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let distance = scrollView.contentSize.height - (targetContentOffset.pointee.y + scrollView.bounds.height)
if !isLoading && distance < 200 && !posts.isEmpty {
loadMoreData()
}
}
}
// MARK: - PostSectionControllerDelegate
extension FeedViewController: PostSectionControllerDelegate {
func postSectionController(_ sectionController: PostSectionController, didSelectOptionsFor post: Post, from sourceView: UIView) {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
self?.deletePost(post)
}
let reportAction = UIAlertAction(title: "Report", style: .default) { _ in
print("Reported post: \(post.id)")
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(deleteAction)
alertController.addAction(reportAction)
alertController.addAction(cancelAction)
// Configure for iPad
if let popoverController = alertController.popoverPresentationController {
// If we have a specific source view (like a button), use it
popoverController.sourceView = sourceView
popoverController.sourceRect = sourceView.bounds
}
present(alertController, animated: true, completion: nil)
}
func postSectionController(_ sectionController: PostSectionController, didRequestDeleteFor post: Post) {
deletePost(post)
}
private func deletePost(_ post: Post) {
if let index = posts.firstIndex(where: { $0.id == post.id }) {
posts.remove(at: index)
adapter.performUpdates(animated: true)
}
}
}