Add pull to refresh for Subscriptions, Popular and Trending (fixes #31)

This commit is contained in:
Arkadiusz Fal 2022-01-05 17:25:57 +01:00
parent 1db4a3197d
commit 363424fa74
13 changed files with 527 additions and 67 deletions

View File

@ -27,11 +27,17 @@ struct FavoritesView: View {
FavoriteItemView(item: item, dragging: $dragging)
}
#else
#if os(iOS)
let first = favorites.first
#endif
ForEach(favorites) { item in
FavoriteItemView(item: item, dragging: $dragging)
#if os(macOS)
.workaroundForVerticalScrollingBug()
#endif
#if os(iOS)
.padding(.top, item == first && RefreshControl.navigationBarTitleDisplayMode == .inline ? 10 : 0)
#endif
}
#endif
}
@ -54,6 +60,9 @@ struct FavoritesView: View {
.background(Color.secondaryBackground)
.frame(minWidth: 360)
#endif
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
}
}
}

View File

@ -127,6 +127,9 @@ struct PlaylistsView: View {
.onChange(of: accounts.current) { _ in
model.load(force: true)
}
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
}
#if os(tvOS)

View File

@ -48,6 +48,69 @@ struct TrendingView: View {
}
}
}
.toolbar {
#if os(macOS)
ToolbarItemGroup {
if let favoriteItem = favoriteItem {
FavoriteButton(item: favoriteItem)
.id(favoriteItem.id)
}
if accounts.app.supportsTrendingCategories {
categoryButton
}
countryButton
}
#elseif os(iOS)
ToolbarItemGroup(placement: .bottomBar) {
Group {
if accounts.app.supportsTrendingCategories {
HStack {
Text("Category")
.foregroundColor(.secondary)
categoryButton
// only way to disable Menu animation is to
// force redraw of the view when it changes
.id(UUID())
}
Spacer()
}
if let favoriteItem = favoriteItem {
FavoriteButton(item: favoriteItem)
.id(favoriteItem.id)
Spacer()
}
HStack {
Text("Country")
.foregroundColor(.secondary)
countryButton
}
}
}
#endif
}
.onChange(of: resource) { _ in
resource.load()
updateFavoriteItem()
}
.onAppear {
if videos.isEmpty {
resource.addObserver(store)
resource.loadIfNeeded()
} else {
store.replace(videos)
}
updateFavoriteItem()
}
#if os(tvOS)
.fullScreenCover(isPresented: $presentingCountrySelection) {
TrendingCountry(selectedCountry: $country)
@ -61,67 +124,14 @@ struct TrendingView: View {
}
.navigationTitle("Trending")
#endif
.toolbar {
#if os(macOS)
ToolbarItemGroup {
if let favoriteItem = favoriteItem {
FavoriteButton(item: favoriteItem)
.id(favoriteItem.id)
}
if accounts.app.supportsTrendingCategories {
categoryButton
}
countryButton
}
#elseif os(iOS)
ToolbarItemGroup(placement: .bottomBar) {
Group {
HStack {
if accounts.app.supportsTrendingCategories {
Text("Category")
.foregroundColor(.secondary)
categoryButton
// only way to disable Menu animation is to
// force redraw of the view when it changes
.id(UUID())
}
}
Spacer()
if let favoriteItem = favoriteItem {
FavoriteButton(item: favoriteItem)
.id(favoriteItem.id)
Spacer()
}
HStack {
Text("Country")
.foregroundColor(.secondary)
countryButton
}
}
}
#endif
}
.onChange(of: resource) { _ in
resource.load()
updateFavoriteItem()
}
.onAppear {
if videos.isEmpty {
resource.addObserver(store)
resource.loadIfNeeded()
} else {
store.replace(videos)
}
updateFavoriteItem()
}
#if os(iOS)
.refreshControl { refreshControl in
resource.load().onCompletion { _ in
refreshControl.endRefreshing()
}
}
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
}
#if os(tvOS)

View File

@ -30,5 +30,13 @@ struct PopularView: View {
FavoriteButton(item: FavoriteItem(section: .popular))
}
}
#if os(iOS)
.refreshControl { refreshControl in
resource?.load().onCompletion { _ in
refreshControl.endRefreshing()
}
}
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
}
}

View File

@ -24,6 +24,13 @@ struct SubscriptionsView: View {
.onChange(of: accounts.current) { _ in
loadResources(force: true)
}
#if os(iOS)
.refreshControl { refreshControl in
loadResources(force: true) {
refreshControl.endRefreshing()
}
}
#endif
}
}
.toolbar {
@ -31,26 +38,35 @@ struct SubscriptionsView: View {
FavoriteButton(item: FavoriteItem(section: .subscriptions))
}
}
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
}
fileprivate func loadResources(force: Bool = false) {
private func loadResources(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
feed?.addObserver(store)
if accounts.app == .invidious {
// Invidious for some reason won't refresh feed until homepage is loaded
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
request.onSuccess { _ in
loadFeed(force: force)
loadFeed(force: force, onCompletion: onCompletion)
}
} else {
loadFeed(force: force)
loadFeed(force: force, onCompletion: onCompletion)
}
} else {
loadFeed(force: force)
loadFeed(force: force, onCompletion: onCompletion)
}
}
fileprivate func loadFeed(force: Bool = false) {
_ = force ? feed?.load() : feed?.loadIfNeeded()
private func loadFeed(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
if let request = force ? feed?.load() : feed?.loadIfNeeded() {
request.onCompletion { _ in
onCompletion()
}
} else {
onCompletion()
}
}
}

View File

@ -0,0 +1,15 @@
//
// UIResponder+Extensions.swift
// SwiftUI_Pull_to_Refresh
//
// Created by Geri Borbás on 21/09/2021.
//
import Foundation
import UIKit
extension UIResponder {
var parentViewController: UIViewController? {
next as? UIViewController ?? next?.parentViewController
}
}

View File

@ -0,0 +1,70 @@
//
// UIView+Extensions.swift
// SwiftUI_Pull_to_Refresh
//
// Created by Geri Borbás on 19/09/2021.
//
import Foundation
import UIKit
extension UIView {
/// Returs frame in screen coordinates.
var globalFrame: CGRect {
if let window = window {
return convert(bounds, to: window.screen.coordinateSpace)
} else {
return .zero
}
}
/// Returns with all the instances of the given view type in the view hierarchy.
func viewsInHierarchy<ViewType: UIView>() -> [ViewType]? {
var views: [ViewType] = []
viewsInHierarchy(views: &views)
return views.isEmpty ? nil : views
}
private func viewsInHierarchy<ViewType: UIView>(views: inout [ViewType]) {
subviews.forEach { eachSubView in
if let matchingView = eachSubView as? ViewType {
views.append(matchingView)
}
eachSubView.viewsInHierarchy(views: &views)
}
}
/// Search ancestral view hierarcy for the given view type.
func searchViewAnchestorsFor<ViewType: UIView>(
_ onViewFound: (ViewType) -> Void
) {
if let matchingView = superview as? ViewType {
onViewFound(matchingView)
} else {
superview?.searchViewAnchestorsFor(onViewFound)
}
}
/// Search ancestral view hierarcy for the given view type.
func searchViewAnchestorsFor<ViewType: UIView>() -> ViewType? {
if let matchingView = superview as? ViewType {
return matchingView
} else {
return superview?.searchViewAnchestorsFor()
}
}
func printViewHierarchyInformation(_ level: Int = 0) {
printViewInformation(level)
subviews.forEach { $0.printViewHierarchyInformation(level + 1) }
}
func printViewInformation(_ level: Int) {
let leadingWhitespace = String(repeating: " ", count: level)
let className = "\(Self.self)"
let superclassName = "\(superclass!)"
let frame = "\(self.frame)"
let id = (accessibilityIdentifier == nil) ? "" : " `\(accessibilityIdentifier!)`"
print("\(leadingWhitespace)\(className): \(superclassName)\(id) \(frame)")
}
}

47
Vendor/RefreshControl/README vendored Normal file
View File

@ -0,0 +1,47 @@
https://github.com/Geri-Borbas/iOS.Blog.SwiftUI_Pull_to_Refresh
# SwiftUI Pull to Refresh
⇣ SwiftUI Pull to Refresh (for iOS 13 and iOS 14) condensed into a single modifier.
Complementary repository for article [**SwiftUI Pull to Refresh**] (in progress). See [`ContentView.swift`] for usage, and [`RefreshControlModifier.swift`] for the source. Designed to work with **multiple scroll views** on the same screen.
```Swift
struct ContentView: View {
var body: some View {
VStack {
HStack {
List {
ForEach(1...100, id: \.self) { eachRowIndex in
Text("Left \(eachRowIndex)")
}
}
.refreshControl { refreshControl in
Network.refresh {
refreshControl.endRefreshing()
}
}
List {
ForEach(1...100, id: \.self) { eachRowIndex in
Text("Right \(eachRowIndex)")
}
}
.refreshControl { refreshControl in
Network.refresh {
refreshControl.endRefreshing()
}
}
}
}
}
}
```
## License
> Licensed under the [**MIT License**](https://en.wikipedia.org/wiki/MIT_License).
[`ContentView.swift`]: SwiftUI_Pull_to_Refresh/Views/ContentView.swift
[`RefreshControl.swift`]: SwiftUI_Pull_to_Refresh/Views/RefreshControl.swift

View File

@ -0,0 +1,56 @@
//
// RefreshControl.swift
// SwiftUI_Pull_to_Refresh
//
// Created by Geri Borbás on 18/09/2021.
//
import Combine
import Foundation
import SwiftUI
import UIKit
final class RefreshControl: ObservableObject {
static var navigationBarTitleDisplayMode: NavigationBarItem.TitleDisplayMode {
if #available(iOS 15.0, *) {
return .automatic
}
return .inline
}
let onValueChanged: (_ refreshControl: UIRefreshControl) -> Void
internal init(onValueChanged: @escaping ((UIRefreshControl) -> Void)) {
self.onValueChanged = onValueChanged
}
/// Adds a `UIRefreshControl` to the `UIScrollView` provided.
func add(to scrollView: UIScrollView) {
scrollView.refreshControl = UIRefreshControl().withTarget(
self,
action: #selector(onValueChangedAction),
for: .valueChanged
)
.testable(as: "RefreshControl")
}
@objc private func onValueChangedAction(sender: UIRefreshControl) {
onValueChanged(sender)
}
}
extension UIRefreshControl {
/// Convinience method to assign target action inline.
func withTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) -> UIRefreshControl {
addTarget(target, action: action, for: controlEvents)
return self
}
/// Convinience method to match refresh control for UI testing.
func testable(as id: String) -> UIRefreshControl {
isAccessibilityElement = true
accessibilityIdentifier = id
return self
}
}

View File

@ -0,0 +1,42 @@
//
// RefreshControlModifier.swift
// SwiftUI_Pull_to_Refresh
//
// Created by Geri Borbás on 18/09/2021.
//
import Foundation
import SwiftUI
struct RefreshControlModifier: ViewModifier {
@State private var geometryReaderFrame: CGRect = .zero
let refreshControl: RefreshControl
internal init(onValueChanged: @escaping (UIRefreshControl) -> Void) {
refreshControl = RefreshControl(onValueChanged: onValueChanged)
}
func body(content: Content) -> some View {
content
.background(
GeometryReader { geometry in
ScrollViewMatcher(
onResolve: { scrollView in
refreshControl.add(to: scrollView)
},
geometryReaderFrame: $geometryReaderFrame
)
.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
.onPreferenceChange(FramePreferenceKey.self) { frame in
self.geometryReaderFrame = frame
}
}
)
}
}
extension View {
func refreshControl(onValueChanged: @escaping (_ refreshControl: UIRefreshControl) -> Void) -> some View {
modifier(RefreshControlModifier(onValueChanged: onValueChanged))
}
}

View File

@ -0,0 +1,18 @@
//
// FramePreferenceKey.swift
// SwiftUI_Pull_to_Refresh
//
// Created by Geri Borbás on 21/09/2021.
//
import Foundation
import SwiftUI
struct FramePreferenceKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue = CGRect.zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}

View File

@ -0,0 +1,106 @@
//
// ScrollViewMatcher.swift
// SwiftUI_Pull_to_Refresh
//
// Created by Geri Borbás on 17/09/2021.
//
import Foundation
import SwiftUI
final class ScrollViewMatcher: UIViewControllerRepresentable {
let onMatch: (UIScrollView) -> Void
@Binding var geometryReaderFrame: CGRect
init(onResolve: @escaping (UIScrollView) -> Void, geometryReaderFrame: Binding<CGRect>) {
onMatch = onResolve
_geometryReaderFrame = geometryReaderFrame
}
func makeUIViewController(context _: Context) -> ScrollViewMatcherViewController {
ScrollViewMatcherViewController(onResolve: onMatch, geometryReaderFrame: geometryReaderFrame)
}
func updateUIViewController(_ viewController: ScrollViewMatcherViewController, context _: Context) {
viewController.geometryReaderFrame = geometryReaderFrame
}
}
final class ScrollViewMatcherViewController: UIViewController {
let onMatch: (UIScrollView) -> Void
private var scrollView: UIScrollView? {
didSet {
if oldValue != scrollView,
let scrollView = scrollView
{
onMatch(scrollView)
}
}
}
var geometryReaderFrame: CGRect {
didSet {
match()
}
}
init(onResolve: @escaping (UIScrollView) -> Void, geometryReaderFrame: CGRect, debug _: Bool = false) {
onMatch = onResolve
self.geometryReaderFrame = geometryReaderFrame
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("Use init(onMatch:) to instantiate ScrollViewMatcherViewController.")
}
func match() {
// matchUsingHierarchy()
matchUsingGeometry()
}
func matchUsingHierarchy() {
if parent != nil {
// Lookup view ancestry for any `UIScrollView`.
view.searchViewAnchestorsFor { (scrollView: UIScrollView) in
self.scrollView = scrollView
}
}
}
func matchUsingGeometry() {
if let parent = parent {
if let scrollViewsInHierarchy: [UIScrollView] = parent.view.viewsInHierarchy() {
// Return first match if only a single scroll view is found in the hierarchy.
if scrollViewsInHierarchy.count == 1,
let firstScrollViewInHierarchy = scrollViewsInHierarchy.first
{
scrollView = firstScrollViewInHierarchy
// Filter by frame origins if multiple matches found.
} else {
if let firstMatchingFrameOrigin = scrollViewsInHierarchy.filter({
$0.globalFrame.origin.close(to: geometryReaderFrame.origin)
}).first {
scrollView = firstMatchingFrameOrigin
}
}
}
}
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
match()
}
}
extension CGPoint {
/// Returns `true` if this point is close the other point (considering a ~1 pt tolerance).
func close(to point: CGPoint) -> Bool {
let inset = Double(1)
let rect = CGRect(x: x - inset, y: y - inset, width: inset * 2, height: inset * 2)
return rect.contains(point)
}
}

View File

@ -515,6 +515,14 @@
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
37DD9DA42785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
37DD9DB12785D58D00539416 /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DAF2785D58D00539416 /* RefreshControl.swift */; };
37DD9DB42785D58D00539416 /* RefreshControlModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB02785D58D00539416 /* RefreshControlModifier.swift */; };
37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB82785D60200539416 /* FramePreferenceKey.swift */; };
37DD9DBB2785D60300539416 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB82785D60200539416 /* FramePreferenceKey.swift */; };
37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB82785D60200539416 /* FramePreferenceKey.swift */; };
37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB92785D60200539416 /* ScrollViewMatcher.swift */; };
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DC22785D63A00539416 /* UIResponder+Extensions.swift */; };
37DD9DCB2785E28C00539416 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DC12785D63A00539416 /* UIView+Extensions.swift */; };
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; };
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
@ -778,6 +786,12 @@
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
37DD9DAF2785D58D00539416 /* RefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControl.swift; sourceTree = "<group>"; };
37DD9DB02785D58D00539416 /* RefreshControlModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControlModifier.swift; sourceTree = "<group>"; };
37DD9DB82785D60200539416 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
37DD9DB92785D60200539416 /* ScrollViewMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewMatcher.swift; sourceTree = "<group>"; };
37DD9DC12785D63A00539416 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
37DD9DC22785D63A00539416 /* UIResponder+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+Extensions.swift"; sourceTree = "<group>"; };
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; };
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
@ -1204,6 +1218,7 @@
3722AEBA274DA312005EA4D6 /* Backports */,
37D4B1B72672CFE300C925CA /* Model */,
37C7A9022679058300E721B4 /* Extensions */,
37DD9DCC2785EE6F00539416 /* Vendor */,
3748186426A762300084E870 /* Fixtures */,
37A3B15827255E7F000FB5EE /* Open in Yattee */,
377FC7D1267A080300A6BBAF /* Frameworks */,
@ -1346,6 +1361,43 @@
path = Gestures;
sourceTree = "<group>";
};
37DD9DAE2785D58D00539416 /* RefreshControl */ = {
isa = PBXGroup;
children = (
37DD9DC02785D63A00539416 /* Extensions */,
37DD9DB72785D60200539416 /* ScrollViewMatcher */,
37DD9DAF2785D58D00539416 /* RefreshControl.swift */,
37DD9DB02785D58D00539416 /* RefreshControlModifier.swift */,
);
path = RefreshControl;
sourceTree = "<group>";
};
37DD9DB72785D60200539416 /* ScrollViewMatcher */ = {
isa = PBXGroup;
children = (
37DD9DB82785D60200539416 /* FramePreferenceKey.swift */,
37DD9DB92785D60200539416 /* ScrollViewMatcher.swift */,
);
path = ScrollViewMatcher;
sourceTree = "<group>";
};
37DD9DC02785D63A00539416 /* Extensions */ = {
isa = PBXGroup;
children = (
37DD9DC22785D63A00539416 /* UIResponder+Extensions.swift */,
37DD9DC12785D63A00539416 /* UIView+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
37DD9DCC2785EE6F00539416 /* Vendor */ = {
isa = PBXGroup;
children = (
37DD9DAE2785D58D00539416 /* RefreshControl */,
);
path = Vendor;
sourceTree = "<group>";
};
37FB283F2721B20800A57617 /* Search */ = {
isa = PBXGroup;
children = (
@ -1846,6 +1898,7 @@
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
37DD9DCB2785E28C00539416 /* UIView+Extensions.swift in Sources */,
3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */,
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
@ -1864,6 +1917,7 @@
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
37DD9DB12785D58D00539416 /* RefreshControl.swift in Sources */,
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */,
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
@ -1881,6 +1935,7 @@
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
37DD9DB42785D58D00539416 /* RefreshControlModifier.swift in Sources */,
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */,
@ -1909,6 +1964,7 @@
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
37AAF29026740715007FC770 /* Channel.swift in Sources */,
37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */,
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
@ -1939,9 +1995,11 @@
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */,
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
37141673267A8E10006CA35D /* Country.swift in Sources */,
@ -2012,6 +2070,7 @@
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
3784CDE327772EE40055BBF2 /* Watch.swift in Sources */,
37DD9DBB2785D60300539416 /* FramePreferenceKey.swift in Sources */,
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
@ -2216,6 +2275,7 @@
files = (
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */,
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,