Создание новостной ленты с Uitableview и Uicollectionview в Ios
Создание динамической ленты новостей является фундаментальной особенностью для бесчисленных приложений iOS, от клиентов социальных сетей до агрегаторов контента и профессиональных новостных приложений. В разработке iOS, и являются двумя основными прокручиваемыми контейнерами для отображения коллекций данных. В то время как они имеют много общего - оба полагаются на делегирование, источники данных и многоразовые ячейки - каждый из них лучше всего подходит для конкретных случаев использования. превосходит представление простого вертикального списка элементов, таких как лента заголовков статей с короткими описаниями. предлагает гораздо более гибкие макеты, включая сетки, горизонтальную прокрутку и пользовательские механизмы, что делает его идеальным для изображений-тяжелых лент новостей или интерфейсов в стиле журналов. Эта статья обеспечивает тщательное, готовое к производству руководство по созданию, настройке и оптимизации ленты новостей с использованием обоих компонентов, с расширенными примерами кода и передовой практикой.
Понимание UITableView и UICollectionView
Прежде чем погрузиться в реализацию, важно понять архитектурные различия и когда выбирать одно из другого. Оба вида являются частью UIKit и следуют шаблону MVC (Model-View-Controller), но их возможности компоновки и API значительно различаются.
Оригинальное название: The Workhorse for Lists
представляет собой один столбец строк, который можно сгруппировать в разделы. Каждая строка соответствует ячейке, и вид автоматически управляет вертикальной прокруткой, повторным использованием ячейки и анимацией строк. Это естественный выбор для новостных лент, где каждый элемент представляет собой согласованную, вертикально уложенную компоновку: заголовок, краткое описание, временную метку и, необязательно, изображение. Протокол предоставляет два основных метода: и . Делегат контролирует высоту строки, выбор и редактирование. С современным UIKit вы также можете использовать саморазмерные ячейки, устанавливая .
UICollectionView: гибкость для сложных макетов
отделяет представление данных от алгоритма компоновки через объект.) по умолчанию компоновка потока () устраивает элементы в сетке или протекающие горизонтальные/вертикальные линии, но вы можете подклассировать ее или использовать современные для многосекционных макетов с уникальным поведением (например, горизонтальная карусель с последующим сеткой. Для лент новостей, которые смешивают типы носителей — видео, изображения, карточки статей — или требуют адаптивного интервала и динамических размеров ячейки, является превосходным выбором. Его протокол источника данных зеркально , но с и . предоставляет методы для размеров ячеек, вставок и интервалов.
Создание базовой новостной ленты
Мы пройдемся по созданию минимального контроллера просмотра новостных лент для каждого компонента. Оба примера предполагают, что вы создаете одноэкранное приложение с Xcode и Swift. Для краткости мы опускаем настройку раскадровки; мы создадим просмотры программно.
Создание новостной ленты с UITableView
Начните с подклассирования и соответствия и . Добавьте в качестве основного подвида. Вот полная реализация, которая использует выборочный массив заголовков статей:
import UIKit
class NewsTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private let tableView = UITableView()
private let newsItems: [String] = [
"Breaking: Major Policy Change Announced",
"Tech Giants Unveil AI Partnership",
"Local Weather Warning: Heavy Rain Expected"
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "News Feed"
setupTableView()
}
private func setupTableView() {
tableView.frame = view.bounds
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.rowHeight = 80
view.addSubview(tableView)
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return newsItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = newsItems[indexPath.row]
cell.textLabel?.numberOfLines = 0
cell.accessoryType = .disclosureIndicator
return cell
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: Path) {
tableView.deselectRow(at: indexPath, animated: true)
// Navigate to article detail (to be implemented)
}
}
В этой базовой версии каждая ячейка отображает одну строку текста. Для производственной ленты новостей вы замените простое настраиваемым подклассом, содержащим ярлыки для заголовка, описания и просмотра изображения. Использование для динамической высоты рекомендуется при изменении длины контента.
Создание новостной ленты с помощью UICollectionView
Для ленты в стиле сетки (обычно с видео-картами с тяжелыми новостями) мы реализуем с использованием . Ниже приведен автономный пример, который отображает набор изображений с заполнителем:
import UIKit
class NewsCollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView!
private let imageNames = ["news1", "news2", "news3", "news4"] // Assume images in asset catalog
override func viewDidLoad() {
super.viewDidLoad()
title = "Image Feed"
setupCollectionView()
}
private func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 8
layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.backgroundColor = .white
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")
view.addSubview(collectionView)
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageNames.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
cell.loadImage(named: imageNames[indexPath.item])
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let padding: CGFloat = 8 * 3 // 3 gaps: left, interitem, right
let availableWidth = view.frame.width - padding
let widthPerItem = availableWidth / 2
return CGSize(width: widthPerItem, height: widthPerItem * 1.2)
}
}
// Custom cell class
class ImageCell: UICollectionViewCell {
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
required init?(coder: NSCoder) { fatalError("init(coder:)") }
func loadImage(named: String) {
imageView.image = UIImage(named: named)
}
}
Этот код создает двухколонную сетку с соотношением сторон 1,2:1 на ячейку. Вы можете настроить на для подачи в стиле карусели. Для реальных приложений замените изображения заполнителей с асинхронной загрузкой изображения через или библиотеки, такие как Kingfisher .
Настройка клеток для более богатой новостной ленты
Простой текстовой ячейки недостаточно для современного новостного приложения. Вам нужно отображать миниатюры, даты публикации, имена источников, а иногда и интерактивные кнопки (поделиться, сохранить). Это требует создания пользовательских подклассов или с правильно выложенными подпросмотрами.
Создание ячейки с характерной статьей (UITableView)
Создайте ячейку, которая включает в себя для большого изображения героя, для заголовка, еще одну для выдержки и небольшую этикетку для метки времени. Используйте автоматограф, чтобы убедиться, что ячейка расширяется правильно. Пример:
class ArticleCell: UITableViewCell {
let heroImageView = UIImageView()
let titleLabel = UILabel()
let excerptLabel = UILabel()
let dateLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) { fatalError() }
private func setupViews() {
heroImageView.contentMode = .scaleAspectFill
heroImageView.clipsToBounds = true
titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
titleLabel.numberOfLines = 2
excerptLabel.font = UIFont.systemFont(ofSize: 14)
excerptLabel.textColor = .gray
excerptLabel.numberOfLines = 3
dateLabel.font = UIFont.systemFont(ofSize: 12)
dateLabel.textColor = .lightGray
for subview in [heroImageView, titleLabel, excerptLabel, dateLabel] {
subview.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(subview)
}
// Constraints – simplified; in practice use stack views or manual anchors
NSLayoutConstraint.activate([
heroImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
heroImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
heroImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
heroImageView.heightAnchor.constraint(equalToConstant: 200),
titleLabel.topAnchor.constraint(equalTo: heroImageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: heroImageView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: heroImageView.trailingAnchor),
excerptLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
excerptLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
excerptLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
dateLabel.topAnchor.constraint(equalTo: excerptLabel.bottomAnchor, constant: 4),
dateLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
dateLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
])
}
func configure(with article: Article) {
titleLabel.text = article.title
excerptLabel.text = article.excerpt
dateLabel.text = article.dateString
// Load image asynchronously (omitted)
}
}
В контроллере просмотра зарегистрируйте эту пользовательскую ячейку и реализуйте соответственно. Установите и , чтобы разрешить переменную высоту.
Адаптация клетки для UICollectionView
Тот же шаблон применим и к . Поскольку представления коллекции могут иметь разные макеты, вы можете спроектировать ячейку в стиле «карты», которая работает как в вертикальном, так и в горизонтальном режимах прокрутки. Используйте , чтобы упростить выравнивание. Вот компактная версия:
class NewsCardCell: UICollectionViewCell {
private let cardView = UIView()
private let imageView = UIImageView()
private let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
cardView.backgroundColor = .systemBackground
cardView.layer.cornerRadius = 10
cardView.clipsToBounds = true
cardView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(cardView)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2
titleLabel.translatesAutoresizingMaskIntoConstraints = false
cardView.addSubview(imageView)
cardView.addSubview(titleLabel)
// Layout
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4),
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4),
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4),
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4),
imageView.topAnchor.constraint(equalTo: cardView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
imageView.heightAnchor.constraint(equalToConstant: 120),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: cardView.bottomAnchor, constant: -8)
])
}
required init?(coder: NSCoder) { fatalError() }
}
С помощью этой ячейки, вид коллекции может отображать канал в стиле Pinterest, если вы предоставляете различные размеры предметов через делегата.
Добавление обновлений Pull-to-Refresh и Real-Time
Современные новостные ленты должны поддерживать жесты обновления и динамическую загрузку. Класс легко интегрируется как с , так и , когда просмотр прокрутки уже подключен к управлению обновлениями. В :
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshNewsFeed), for: .valueChanged)
tableView.refreshControl = refreshControl
// or collectionView.refreshControl = refreshControl
В методе , выполнить асинхронный вывод данных (например, из REST API), затем перезагрузить вид на главной очереди и вызвать . Пример:
@objc private func refreshNewsFeed() {
NewsAPIClient.fetchLatestArticles { [weak self] articles in
DispatchQueue.main.async {
self?.newsItems = articles
self?.tableView.reloadData()
self?.tableView.refreshControl?.endRefreshing()
}
}
}
Для обновлений в реальном времени, таких как новости в прямом эфире, рассмотрите возможность использования или сторонней библиотеки, такой как База данных Firebase в реальном времени.
// For UITableView
tableView.performBatchUpdates({
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top)
}, completion: nil)
// For UICollectionView
collectionView.performBatchUpdates({
collectionView.insertItems(at: [IndexPath(item: 0, section: 0)])
}, completion: nil)
Обработка пользовательских взаимодействий и навигации
Когда пользователь нажимает на новостной элемент, приложение должно перейти к просмотру деталей. Внедрить метод делегата и использовать либо раскадровку, либо программный толчок. Для таблиц и коллекций метод делегата обеспечивает путь индекса. Пример для простого :
// In UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let article = newsItems[indexPath.row]
let detailVC = ArticleDetailViewController(article: article)
navigationController?.pushViewController(detailVC, animated: true)
}
// In UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let article = newsItems[indexPath.item]
let detailVC = ArticleDetailViewController(article: article)
navigationController?.pushViewController(detailVC, animated: true)
}
Рассмотрите возможность добавления жестов длинного нажатия для дополнительных действий (обмена, закладки) через , прикрепленных к коллекции или просмотру таблицы, или используйте контекстные меню, введенные в iOS 13.
Соображения производительности для крупных кормов
Новостные ленты могут вырасти до сотен или тысяч элементов. И и повторно используют ячейки, чтобы сохранить память на низком уровне, но вы все равно должны избегать блокировки основной нити.
- Асинхронная загрузка изображения: Никогда не загружайте изображения из сети в главную очередь. Используйте библиотеки, такие как Kingfisher или SDWebImage, которые автоматически обрабатывают кэширование, предварительную выборку и отмену.
- Префетчинг: Соответствует или , чтобы начать загрузку данных для офскриновых ячеек до их появления.
- Эффективно вычислить размеры: Для ячеек с переменной высотой используйте Auto Layout с . В представлениях коллекции кэш вычислял размеры, чтобы избежать повторных расчетов компоновки.
- Ограничить визуализацию невидимых ячеек: Переопределить в пользовательских ячейках для отмены загрузки изображений и сброса состояния.
- Использовать дифференцируемые источники данных (см. следующий раздел) для уменьшения перезагрузки вызовов данных и выполнения анимированных обновлений с минимальным мерцанием.
Современные подходы: композиционное слое и дифференцируемый источник данных
iOS 13 и более поздние версии представили два мощных API, которые значительно упрощают сложные новостные ленты: и (которые также работают с через ).
UICollectionViewКомпозиционный макет
Эта компоновка позволяет определять секции с различным поведением — например, секция баннера героя, которая прокручивается горизонтально, сетка новостей и вертикальный список последних статей. Каждый раздел определяется , который содержит группы и элементы.
let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) -> NSCollectionLayoutSection? in
switch sectionIndex {
case 0:
// Horizontal carousel
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), heightDimension: .absolute(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 10
return section
case 1:
// Vertical grid (2 columns)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(150))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(150))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(8)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 8
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
return section
default:
return nil
}
}
collectionView.collectionViewLayout = layout
Диффабельный источник данных
Вместо ручного управления и управления индексным путем, дифференцируемые источники данных позволяют применять снимки. Это устраняет несоответствия и упрощает анимацию. Пример для :
enum Section {
case featured
case latest
}
struct Article: Hashable {
let id: UUID
let title: String
// ...
}
var dataSource: UICollectionViewDiffableDataSource!
// In viewDidLoad:
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
(collectionView, indexPath, article) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ArticleCell
cell.configure(with: article)
return cell
}
// When data arrives:
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.featured, .latest])
snapshot.appendItems(featuredArticles, toSection: .featured)
snapshot.appendItems(latestArticles, toSection: .latest)
dataSource.apply(snapshot, animatingDifferences: true)
Диффабельные источники данных также могут использоваться с через . Эта схема теперь рекомендована Apple для всех новых проектов, поскольку она устраняет распространенные ошибки и упрощает управление состоянием.
Интеграция с реальными источниками данных
Чтобы сделать вашу новостную ленту динамичной, подключите ее к backend API. Общие подходы включают в себя:
- REST API: Используйте , чтобы принести JSON, декодируйте его и заполните массив моделей. Пример с (iOS 15+):
func fetchArticles() async throws -> [Article] {
let url = URL(string: "https://api.example.com/news")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Article].self, from: data)
}
- Основные данные / CloudKit: Для автономных каналов сначала храните статьи локально с использованием основных данных, а затем синхронизируйте их с облачным бэкэндом. Используйте с для автоматического обновления пользовательского интерфейса при изменении данных.
- Firebase Firestore: Слушатели в режиме реального времени могут напрямую обновлять снимок просмотра коллекции.
Всегда исправляйте ошибки изящно — покажите заполнитель, кнопку повторного использования или кэшированную версию корма.
Продвинутые кастомизации и польский
Чтобы сделать вашу ленту новостей выделяющейся, рассмотрите следующие улучшения:
- Переход анимированных клеток: Используйте или метод сбора изображения , чтобы исчезнуть в клетках по мере их появления.
- Контекстные меню и действия свайпа: На , реализовать , чтобы пометить статьи как прочитанные или сохранить их., использовать через делегата.
- Легкие заголовки разделов: Для группированных кормов сделайте заголовки разделов «липкими», используя простой стиль просмотра таблицы или настроив на композиционную компоновку.
- Темный режим и динамический тип: Используйте и , чтобы обеспечить соблюдение системных настроек корма.
Тестирование вашей новостной ленты
Напишите модульные тесты для логики источника данных и снимка для визуальных регрессий. Используйте симулятор для тестирования различных размеров и ориентаций устройства. Для тестирования производительности, инструмент с профилером времени и проверьте наличие выпавших кадров во время прокрутки.
Заключение
Создание надежной новостной ленты в iOS требует четкого понимания и , наряду с современными инструментами, такими как композиционные макеты и дифференцируемые источники данных. Следуя шаблонам, изложенным в этом руководстве — дизайн пользовательских ячеек, асинхронная загрузка данных, перезагрузка и тщательная настройка производительности — вы можете создавать привлекательные, отзывчивые новостные ленты, которые радуют пользователей. Официальная документация Apple UITableView и UICollectionView являются бесценными ссылками, как и ресурсы сообщества, такие как . С помощью этих инструментов вы хорошо оснащены для создания любой новостной ленты, требуемой вашим приложением.