mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Trends in explore
This commit is contained in:
parent
72c08642d8
commit
ea366368c8
19 changed files with 554 additions and 3 deletions
|
@ -202,6 +202,24 @@ extension ContentDatabase {
|
||||||
t.column("statusId").references("statusRecord", onDelete: .cascade)
|
t.column("statusId").references("statusRecord", onDelete: .cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "instanceRecord") { t in
|
||||||
|
t.column("uri", .text).primaryKey(onConflict: .replace)
|
||||||
|
t.column("title", .text).notNull()
|
||||||
|
t.column("description", .text).notNull()
|
||||||
|
t.column("shortDescription", .text)
|
||||||
|
t.column("email", .text).notNull()
|
||||||
|
t.column("version", .text).notNull()
|
||||||
|
t.column("languages", .blob).notNull()
|
||||||
|
t.column("registrations", .boolean).notNull()
|
||||||
|
t.column("approvalRequired", .boolean).notNull()
|
||||||
|
t.column("invitesEnabled", .boolean).notNull()
|
||||||
|
t.column("urls", .blob).notNull()
|
||||||
|
t.column("stats", .blob).notNull()
|
||||||
|
t.column("thumbnail", .text)
|
||||||
|
t.column("contactAccountId", .text).references("accountRecord", onDelete: .cascade)
|
||||||
|
t.column("maxTootChars", .integer)
|
||||||
|
}
|
||||||
|
|
||||||
try db.create(table: "statusAncestorJoin") { t in
|
try db.create(table: "statusAncestorJoin") { t in
|
||||||
t.column("parentId", .text).indexed().notNull()
|
t.column("parentId", .text).indexed().notNull()
|
||||||
.references("statusRecord", onDelete: .cascade)
|
.references("statusRecord", onDelete: .cascade)
|
||||||
|
|
|
@ -439,6 +439,12 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insert(instance: Instance) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher(updates: instance.save)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[CollectionSection], Error> {
|
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[CollectionSection], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||||
|
@ -611,6 +617,16 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func instancePublisher(uri: String) -> AnyPublisher<Instance, Error> {
|
||||||
|
ValueObservation.tracking(
|
||||||
|
InstanceInfo.request(InstanceRecord.filter(InstanceRecord.Columns.uri == uri)).fetchOne)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.compactMap { $0 }
|
||||||
|
.map(Instance.init(info:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
Emoji.filter(Emoji.Columns.visibleInPicker == true)
|
Emoji.filter(Emoji.Columns.visibleInPicker == true)
|
||||||
|
|
20
DB/Sources/DB/Content/InstanceInfo.swift
Normal file
20
DB/Sources/DB/Content/InstanceInfo.swift
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct InstanceInfo: Codable, Hashable, FetchableRecord {
|
||||||
|
let record: InstanceRecord
|
||||||
|
let contactAccountInfo: AccountInfo?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstanceInfo {
|
||||||
|
static func addingIncludes<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == InstanceRecord {
|
||||||
|
request.including(optional: AccountInfo.addingIncludes(InstanceRecord.contactAccount)
|
||||||
|
.forKey(CodingKeys.contactAccountInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func request(_ request: QueryInterfaceRequest<InstanceRecord>) -> QueryInterfaceRequest<Self> {
|
||||||
|
addingIncludes(request).asRequest(of: self)
|
||||||
|
}
|
||||||
|
}
|
63
DB/Sources/DB/Content/InstanceRecord.swift
Normal file
63
DB/Sources/DB/Content/InstanceRecord.swift
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct InstanceRecord: ContentDatabaseRecord, Hashable {
|
||||||
|
let uri: String
|
||||||
|
let title: String
|
||||||
|
let description: String
|
||||||
|
let shortDescription: String?
|
||||||
|
let email: String
|
||||||
|
let version: String
|
||||||
|
let languages: [String]
|
||||||
|
let registrations: Bool
|
||||||
|
let approvalRequired: Bool
|
||||||
|
let invitesEnabled: Bool
|
||||||
|
let urls: Instance.URLs
|
||||||
|
let stats: Instance.Stats
|
||||||
|
let thumbnail: URL?
|
||||||
|
let contactAccountId: Account.Id?
|
||||||
|
let maxTootChars: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstanceRecord {
|
||||||
|
enum Columns {
|
||||||
|
static let uri = Column(CodingKeys.uri)
|
||||||
|
static let title = Column(CodingKeys.title)
|
||||||
|
static let description = Column(CodingKeys.description)
|
||||||
|
static let shortDescription = Column(CodingKeys.shortDescription)
|
||||||
|
static let email = Column(CodingKeys.email)
|
||||||
|
static let version = Column(CodingKeys.version)
|
||||||
|
static let languages = Column(CodingKeys.languages)
|
||||||
|
static let registrations = Column(CodingKeys.registrations)
|
||||||
|
static let approvalRequired = Column(CodingKeys.approvalRequired)
|
||||||
|
static let invitesEnabled = Column(CodingKeys.invitesEnabled)
|
||||||
|
static let urls = Column(CodingKeys.urls)
|
||||||
|
static let stats = Column(CodingKeys.stats)
|
||||||
|
static let thumbnail = Column(CodingKeys.thumbnail)
|
||||||
|
static let contactAccountId = Column(CodingKeys.contactAccountId)
|
||||||
|
static let maxTootChars = Column(CodingKeys.maxTootChars)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let contactAccount = belongsTo(AccountRecord.self)
|
||||||
|
|
||||||
|
init(instance: Instance) {
|
||||||
|
self.uri = instance.uri
|
||||||
|
self.title = instance.title
|
||||||
|
self.description = instance.description
|
||||||
|
self.shortDescription = instance.shortDescription
|
||||||
|
self.email = instance.email
|
||||||
|
self.version = instance.version
|
||||||
|
self.languages = instance.languages
|
||||||
|
self.registrations = instance.registrations
|
||||||
|
self.approvalRequired = instance.approvalRequired
|
||||||
|
self.invitesEnabled = instance.invitesEnabled
|
||||||
|
self.urls = instance.urls
|
||||||
|
self.stats = instance.stats
|
||||||
|
self.thumbnail = instance.thumbnail
|
||||||
|
self.contactAccountId = instance.contactAccount?.id
|
||||||
|
self.maxTootChars = instance.maxTootChars
|
||||||
|
}
|
||||||
|
}
|
41
DB/Sources/DB/Extensions/Instance+Extensions.swift
Normal file
41
DB/Sources/DB/Extensions/Instance+Extensions.swift
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Instance {
|
||||||
|
func save(_ db: Database) throws {
|
||||||
|
if let contactAccount = contactAccount {
|
||||||
|
try AccountRecord(account: contactAccount).save(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
try InstanceRecord(instance: self).save(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(info: InstanceInfo) {
|
||||||
|
var contactAccount: Account?
|
||||||
|
|
||||||
|
if let contactAccountInfo = info.contactAccountInfo {
|
||||||
|
contactAccount = Account(info: contactAccountInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(record: info.record, contactAccount: contactAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Instance {
|
||||||
|
init(record: InstanceRecord, contactAccount: Account?) {
|
||||||
|
self.init(uri: record.uri,
|
||||||
|
title: record.title,
|
||||||
|
description: record.description,
|
||||||
|
shortDescription: record.shortDescription,
|
||||||
|
email: record.email,
|
||||||
|
version: record.version,
|
||||||
|
urls: record.urls,
|
||||||
|
stats: record.stats,
|
||||||
|
thumbnail: record.thumbnail,
|
||||||
|
contactAccount: contactAccount,
|
||||||
|
maxTootChars: record.maxTootChars)
|
||||||
|
}
|
||||||
|
}
|
70
Data Sources/ExploreDataSource.swift
Normal file
70
Data Sources/ExploreDataSource.swift
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Mastodon
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class ExploreDataSource: UICollectionViewDiffableDataSource<ExploreViewModel.Section, ExploreViewModel.Item> {
|
||||||
|
private let updateQueue =
|
||||||
|
DispatchQueue(label: "com.metabolist.metatext.explore-data-source.update-queue")
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(collectionView: UICollectionView, viewModel: ExploreViewModel) {
|
||||||
|
let tagRegistration = UICollectionView.CellRegistration<TagCollectionViewCell, TagViewModel> {
|
||||||
|
$0.viewModel = $2
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(collectionView: collectionView) {
|
||||||
|
switch $2 {
|
||||||
|
case let .tag(tag):
|
||||||
|
return $0.dequeueConfiguredReusableCell(
|
||||||
|
using: tagRegistration,
|
||||||
|
for: $1,
|
||||||
|
item: viewModel.viewModel(tag: tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerRegistration = UICollectionView.SupplementaryRegistration
|
||||||
|
<ExploreSectionHeaderView>(elementKind: "Header") { [weak self] in
|
||||||
|
$0.label.text = self?.snapshot().sectionIdentifiers[$2.section].displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
supplementaryViewProvider = {
|
||||||
|
$0.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: $2)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.$trends.sink { [weak self] tags in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<ExploreViewModel.Section, ExploreViewModel.Item>()
|
||||||
|
|
||||||
|
if !tags.isEmpty {
|
||||||
|
snapshot.appendSections([.trending])
|
||||||
|
snapshot.appendItems(tags.map(ExploreViewModel.Item.tag), toSection: .trending)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<ExploreViewModel.Section, ExploreViewModel.Item>,
|
||||||
|
animatingDifferences: Bool = true,
|
||||||
|
completion: (() -> Void)? = nil) {
|
||||||
|
updateQueue.async {
|
||||||
|
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExploreViewModel.Section {
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .trending:
|
||||||
|
return NSLocalizedString("explore.trending", comment: "")
|
||||||
|
case .instance:
|
||||||
|
return NSLocalizedString("explore.instance", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,6 +74,8 @@
|
||||||
"emoji.system-group.objects" = "Objects";
|
"emoji.system-group.objects" = "Objects";
|
||||||
"emoji.system-group.symbols" = "Symbols";
|
"emoji.system-group.symbols" = "Symbols";
|
||||||
"emoji.system-group.flags" = "Flags";
|
"emoji.system-group.flags" = "Flags";
|
||||||
|
"explore.trending" = "Trending Now";
|
||||||
|
"explore.instance" = "Instance";
|
||||||
"error" = "Error";
|
"error" = "Error";
|
||||||
"favorites" = "Favorites";
|
"favorites" = "Favorites";
|
||||||
"follow-requests" = "Follow Requests";
|
"follow-requests" = "Follow Requests";
|
||||||
|
|
|
@ -28,4 +28,28 @@ public struct Instance: Codable, Hashable {
|
||||||
public let thumbnail: URL?
|
public let thumbnail: URL?
|
||||||
public let contactAccount: Account?
|
public let contactAccount: Account?
|
||||||
public let maxTootChars: Int?
|
public let maxTootChars: Int?
|
||||||
|
|
||||||
|
public init(uri: String,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
shortDescription: String?,
|
||||||
|
email: String,
|
||||||
|
version: String,
|
||||||
|
urls: Instance.URLs,
|
||||||
|
stats: Instance.Stats,
|
||||||
|
thumbnail: URL?,
|
||||||
|
contactAccount: Account?,
|
||||||
|
maxTootChars: Int?) {
|
||||||
|
self.uri = uri
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.shortDescription = shortDescription
|
||||||
|
self.email = email
|
||||||
|
self.version = version
|
||||||
|
self.urls = urls
|
||||||
|
self.stats = stats
|
||||||
|
self.thumbnail = thumbnail
|
||||||
|
self.contactAccount = contactAccount
|
||||||
|
self.maxTootChars = maxTootChars
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
25
MastodonAPI/Sources/MastodonAPI/Endpoints/TagsEndpoint.swift
Normal file
25
MastodonAPI/Sources/MastodonAPI/Endpoints/TagsEndpoint.swift
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum TagsEndpoint {
|
||||||
|
case trends
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TagsEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [Tag]
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .trends: return ["trends"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .trends: return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,6 +145,9 @@
|
||||||
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */; };
|
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */; };
|
||||||
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */; };
|
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */; };
|
||||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
|
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
|
||||||
|
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */; };
|
||||||
|
D0DDA77525C5F73F00FA0F91 /* TagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */; };
|
||||||
|
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */; };
|
||||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
||||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||||
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
||||||
|
@ -332,6 +335,9 @@
|
||||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = "<group>"; };
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = "<group>"; };
|
||||||
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
||||||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
||||||
|
D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreDataSource.swift; sourceTree = "<group>"; };
|
||||||
|
D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreSectionHeaderView.swift; sourceTree = "<group>"; };
|
||||||
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
||||||
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
||||||
|
@ -415,6 +421,7 @@
|
||||||
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
||||||
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
||||||
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
||||||
|
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
||||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
||||||
|
@ -485,6 +492,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
||||||
|
D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = "Collection View Cells";
|
path = "Collection View Cells";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -610,6 +618,7 @@
|
||||||
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */,
|
||||||
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */,
|
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */,
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||||
);
|
);
|
||||||
|
@ -934,6 +943,7 @@
|
||||||
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
|
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
|
||||||
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
|
D0DDA77525C5F73F00FA0F91 /* TagCollectionViewCell.swift in Sources */,
|
||||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||||
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
|
@ -1021,6 +1031,7 @@
|
||||||
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */,
|
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */,
|
||||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
||||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
||||||
|
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */,
|
||||||
D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */,
|
D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */,
|
||||||
D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */,
|
D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */,
|
||||||
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */,
|
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */,
|
||||||
|
@ -1029,6 +1040,7 @@
|
||||||
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
||||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||||
|
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */,
|
||||||
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */,
|
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */,
|
||||||
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
||||||
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
|
|
|
@ -7,12 +7,15 @@ import Mastodon
|
||||||
import MastodonAPI
|
import MastodonAPI
|
||||||
|
|
||||||
public struct ExploreService {
|
public struct ExploreService {
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.mastodonAPIClient = mastodonAPIClient
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
self.contentDatabase = contentDatabase
|
self.contentDatabase = contentDatabase
|
||||||
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,4 +23,14 @@ public extension ExploreService {
|
||||||
func searchService() -> SearchService {
|
func searchService() -> SearchService {
|
||||||
SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func instanceServicePublisher(uri: String) -> AnyPublisher<InstanceService, Error> {
|
||||||
|
contentDatabase.instancePublisher(uri: uri)
|
||||||
|
.map { InstanceService(instance: $0, mastodonAPIClient: mastodonAPIClient) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchTrends() -> AnyPublisher<[Tag], Error> {
|
||||||
|
mastodonAPIClient.request(TagsEndpoint.trends)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,10 @@ public extension IdentityService {
|
||||||
|
|
||||||
func refreshInstance() -> AnyPublisher<Never, Error> {
|
func refreshInstance() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(InstanceEndpoint.instance)
|
mastodonAPIClient.request(InstanceEndpoint.instance)
|
||||||
.flatMap { identityDatabase.updateInstance($0, id: id) }
|
.flatMap {
|
||||||
|
identityDatabase.updateInstance($0, id: id)
|
||||||
|
.merge(with: contentDatabase.insert(instance: $0))
|
||||||
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct InstanceService {
|
||||||
|
public let instance: Instance
|
||||||
|
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
|
||||||
|
init(instance: Instance, mastodonAPIClient: MastodonAPIClient) {
|
||||||
|
self.instance = instance
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,11 +9,15 @@ final class ExploreViewController: UICollectionViewController {
|
||||||
private let rootViewModel: RootViewModel
|
private let rootViewModel: RootViewModel
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private lazy var dataSource: ExploreDataSource = {
|
||||||
|
.init(collectionView: collectionView, viewModel: viewModel)
|
||||||
|
}()
|
||||||
|
|
||||||
init(viewModel: ExploreViewModel, rootViewModel: RootViewModel) {
|
init(viewModel: ExploreViewModel, rootViewModel: RootViewModel) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.rootViewModel = rootViewModel
|
self.rootViewModel = rootViewModel
|
||||||
|
|
||||||
super.init(collectionViewLayout: UICollectionViewFlowLayout())
|
super.init(collectionViewLayout: Self.layout())
|
||||||
|
|
||||||
tabBarItem = UITabBarItem(
|
tabBarItem = UITabBarItem(
|
||||||
title: NSLocalizedString("main-navigation.explore", comment: ""),
|
title: NSLocalizedString("main-navigation.explore", comment: ""),
|
||||||
|
@ -29,6 +33,16 @@ final class ExploreViewController: UICollectionViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
collectionView.dataSource = dataSource
|
||||||
|
collectionView.backgroundColor = .systemBackground
|
||||||
|
clearsSelectionOnViewWillAppear = true
|
||||||
|
|
||||||
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
|
collectionView.refreshControl?.addAction(
|
||||||
|
UIAction { [weak self] _ in
|
||||||
|
self?.viewModel.refresh() },
|
||||||
|
for: .valueChanged)
|
||||||
|
|
||||||
navigationItem.title = NSLocalizedString("main-navigation.explore", comment: "")
|
navigationItem.title = NSLocalizedString("main-navigation.explore", comment: "")
|
||||||
|
|
||||||
let searchResultsController = TableViewController(
|
let searchResultsController = TableViewController(
|
||||||
|
@ -43,6 +57,19 @@ final class ExploreViewController: UICollectionViewController {
|
||||||
searchController.searchResultsUpdater = self
|
searchController.searchResultsUpdater = self
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
|
||||||
|
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$loading.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let refreshControlVisibile = self.collectionView.refreshControl?.isRefreshing ?? false
|
||||||
|
|
||||||
|
if !$0, refreshControlVisibile {
|
||||||
|
self.collectionView.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.searchViewModel.events.sink { [weak self] in
|
viewModel.searchViewModel.events.sink { [weak self] in
|
||||||
if case let .navigation(navigation) = $0,
|
if case let .navigation(navigation) = $0,
|
||||||
case let .searchScope(scope) = navigation {
|
case let .searchScope(scope) = navigation {
|
||||||
|
@ -52,6 +79,18 @@ final class ExploreViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
viewModel.select(item: item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExploreViewController: UISearchResultsUpdating {
|
extension ExploreViewController: UISearchResultsUpdating {
|
||||||
|
@ -68,3 +107,36 @@ extension ExploreViewController: UISearchResultsUpdating {
|
||||||
viewModel.searchViewModel.query = searchController.searchBar.text ?? ""
|
viewModel.searchViewModel.query = searchController.searchBar.text ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ExploreViewController {
|
||||||
|
static func layout() -> UICollectionViewLayout {
|
||||||
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
|
||||||
|
config.headerMode = .supplementary
|
||||||
|
|
||||||
|
return UICollectionViewCompositionalLayout.list(using: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(event: ExploreViewModel.Event) {
|
||||||
|
switch event {
|
||||||
|
case let .navigation(navigation):
|
||||||
|
handle(navigation: navigation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(navigation: Navigation) {
|
||||||
|
switch navigation {
|
||||||
|
case let .collection(collectionService):
|
||||||
|
let vc = TableViewController(
|
||||||
|
viewModel: CollectionItemsViewModel(
|
||||||
|
collectionService: collectionService,
|
||||||
|
identityContext: viewModel.identityContext),
|
||||||
|
rootViewModel: rootViewModel,
|
||||||
|
parentNavigationController: nil)
|
||||||
|
|
||||||
|
show(vc, sender: self)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public final class ExploreViewModel: ObservableObject {
|
public final class ExploreViewModel: ObservableObject {
|
||||||
public let searchViewModel: SearchViewModel
|
public let searchViewModel: SearchViewModel
|
||||||
|
public let events: AnyPublisher<Event, Never>
|
||||||
|
@Published public var instanceViewModel: InstanceViewModel?
|
||||||
|
@Published public var trends = [Tag]()
|
||||||
|
@Published public private(set) var loading = false
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
public let identityContext: IdentityContext
|
public let identityContext: IdentityContext
|
||||||
|
|
||||||
private let exploreService: ExploreService
|
private let exploreService: ExploreService
|
||||||
|
private let eventsSubject = PassthroughSubject<Event, Never>()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(service: ExploreService, identityContext: IdentityContext) {
|
init(service: ExploreService, identityContext: IdentityContext) {
|
||||||
exploreService = service
|
exploreService = service
|
||||||
|
@ -15,5 +24,60 @@ public final class ExploreViewModel: ObservableObject {
|
||||||
searchViewModel = SearchViewModel(
|
searchViewModel = SearchViewModel(
|
||||||
searchService: exploreService.searchService(),
|
searchService: exploreService.searchService(),
|
||||||
identityContext: identityContext)
|
identityContext: identityContext)
|
||||||
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
identityContext.$identity
|
||||||
|
.compactMap { $0.instance?.uri }
|
||||||
|
.removeDuplicates()
|
||||||
|
.flatMap { service.instanceServicePublisher(uri: $0) }
|
||||||
|
.map { InstanceViewModel(instanceService: $0) }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$instanceViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ExploreViewModel {
|
||||||
|
enum Event {
|
||||||
|
case navigation(Navigation)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Section: Hashable {
|
||||||
|
case trending
|
||||||
|
case instance
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Item: Hashable {
|
||||||
|
case tag(Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
exploreService.fetchTrends()
|
||||||
|
.handleEvents(receiveOutput: { [weak self] trends in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.trends = trends
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ignoreOutput()
|
||||||
|
.merge(with: identityContext.service.refreshInstance())
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true },
|
||||||
|
receiveCompletion: { [weak self] _ in self?.loading = false })
|
||||||
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewModel(tag: Tag) -> TagViewModel {
|
||||||
|
.init(tag: tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func select(item: ExploreViewModel.Item) {
|
||||||
|
switch item {
|
||||||
|
case let .tag(tag):
|
||||||
|
eventsSubject.send(
|
||||||
|
.navigation(.collection(exploreService
|
||||||
|
.navigationService
|
||||||
|
.timelineService(timeline: .tag(tag.name)))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class InstanceViewModel: ObservableObject {
|
||||||
|
private let instanceService: InstanceService
|
||||||
|
|
||||||
|
init(instanceService: InstanceService) {
|
||||||
|
self.instanceService = instanceService
|
||||||
|
}
|
||||||
|
}
|
|
@ -121,7 +121,7 @@ public extension NavigationViewModel {
|
||||||
service: identityContext.service.exploreService(),
|
service: identityContext.service.exploreService(),
|
||||||
identityContext: identityContext)
|
identityContext: identityContext)
|
||||||
|
|
||||||
// TODO: initial request
|
exploreViewModel.refresh()
|
||||||
|
|
||||||
return exploreViewModel
|
return exploreViewModel
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class TagCollectionViewCell: UICollectionViewListCell {
|
||||||
|
var viewModel: TagViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = TagContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
|
updateConstraintsIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateConstraints() {
|
||||||
|
super.updateConstraints()
|
||||||
|
|
||||||
|
let separatorLeadingAnchor: NSLayoutXAxisAnchor
|
||||||
|
let separatorTrailingAnchor: NSLayoutXAxisAnchor
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
separatorLeadingAnchor = readableContentGuide.leadingAnchor
|
||||||
|
separatorTrailingAnchor = readableContentGuide.trailingAnchor
|
||||||
|
} else {
|
||||||
|
separatorLeadingAnchor = leadingAnchor
|
||||||
|
separatorTrailingAnchor = trailingAnchor
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLayoutGuide.leadingAnchor.constraint(equalTo: separatorLeadingAnchor),
|
||||||
|
separatorLayoutGuide.trailingAnchor.constraint(equalTo: separatorTrailingAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
44
Views/UIKit/ExploreSectionHeaderView.swift
Normal file
44
Views/UIKit/ExploreSectionHeaderView.swift
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ExploreSectionHeaderView: UICollectionReusableView {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExploreSectionHeaderView {
|
||||||
|
func initialSetup() {
|
||||||
|
backgroundColor = .systemBackground
|
||||||
|
|
||||||
|
addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
|
||||||
|
let layoutGuide: UILayoutGuide
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
layoutGuide = readableContentGuide
|
||||||
|
} else {
|
||||||
|
layoutGuide = layoutMarginsGuide
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
|
||||||
|
label.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
|
||||||
|
label.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
|
||||||
|
label.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue