Ferrite: Properly inline lists

The inset grouped list style has a top inset that adds extra space
between the navigation bar title and the list rows. Use introspect
to remove this space on UITableView and UICollectionView (for iOS 16).

Sections completely ignore the introspect changes, so add a section
header which removes the list row insets.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2022-09-05 12:23:05 -04:00
parent b4fe3807d6
commit 16a39c3a58
13 changed files with 158 additions and 19 deletions

View file

@ -71,6 +71,10 @@
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; }; 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; };
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; };
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; };
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; };
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
@ -137,6 +141,9 @@
0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; }; 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; };
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
@ -153,6 +160,7 @@
0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C64A4B4288903680079976D /* Base32 in Frameworks */,
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */,
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */,
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
); );
@ -241,6 +249,9 @@
0C32FB562890D1F2002BD219 /* ListRowViews.swift */, 0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */, 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */,
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */,
0CB6516228C5A57300DCA721 /* ConditionalId.swift */,
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
); );
path = CommonViews; path = CommonViews;
sourceTree = "<group>"; sourceTree = "<group>";
@ -366,6 +377,7 @@
0C4CFC452897030D00AD9FAD /* Regex */, 0C4CFC452897030D00AD9FAD /* Regex */,
0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7376EF28A97D1400D60918 /* SwiftUIX */,
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
0CB6516728C5A5EC00DCA721 /* Introspect */,
); );
productName = Torrenter; productName = Torrenter;
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
@ -402,6 +414,7 @@
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */, 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */,
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
); );
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -431,8 +444,10 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */, 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */,
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
@ -454,6 +469,7 @@
0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */,
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
@ -747,6 +763,14 @@
minimumVersion = 2.0.0; minimumVersion = 2.0.0;
}; };
}; };
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect";
requirement = {
branch = master;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -780,6 +804,11 @@
package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */; package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup; productName = SwiftSoup;
}; };
0CB6516728C5A5EC00DCA721 /* Introspect */ = {
isa = XCSwiftPackageProductDependency;
package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
productName = Introspect;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */

View file

@ -6,11 +6,34 @@
// //
import SwiftUI import SwiftUI
import Introspect
extension View { extension View {
// MARK: Custom introspect functions
func introspectCollectionView(customize: @escaping (UICollectionView) -> ()) -> some View {
return inject(UIKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost)
},
customize: customize
))
}
// MARK: Modifiers // MARK: Modifiers
func dynamicAccentColor(_ color: Color) -> some View { func dynamicAccentColor(_ color: Color) -> some View {
modifier(DynamicAccentColor(color: color)) modifier(DynamicAccentColor(color: color))
} }
func conditionalId<ID: Hashable>(_ id: ID) -> some View {
modifier(ConditionalId(id: id))
}
func inlinedList() -> some View {
modifier(InlinedList())
}
} }

View file

@ -0,0 +1,24 @@
//
// ConditionalId.swift
// Ferrite
//
// Created by Brian Dashore on 9/4/22.
//
// Only applies an ID for below iOS 16
// This is due to ID workarounds making iOS 16 apps crash
//
import SwiftUI
struct ConditionalId<ID: Hashable>: ViewModifier {
let id: ID
func body(content: Content) -> some View {
if #available(iOS 16, *) {
content
} else {
content
.id(id)
}
}
}

View file

@ -0,0 +1,30 @@
//
// InlineHeader.swift
// Ferrite
//
// Created by Brian Dashore on 9/5/22.
//
import SwiftUI
struct InlineHeader: View {
let title: String
init(_ title: String) {
self.title = title
}
var body: some View {
Group {
if #available(iOS 16, *) {
Text(title)
.padding(.vertical, 5)
} else {
Text(title)
.padding(.vertical, 10)
}
}
.padding(.horizontal, 20)
.listRowInsets(EdgeInsets())
}
}

View file

@ -0,0 +1,28 @@
//
// InlinedList.swift
// Ferrite
//
// Created by Brian Dashore on 9/4/22.
//
// Removes the top padding on lists for iOS 16
// Use UITableView.appearance().contentInset.top = -20 for iOS 15 and below in the App file
//
import SwiftUI
import Introspect
struct InlinedList: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16, *) {
content
.introspectCollectionView { collectionView in
collectionView.contentInset.top = -20
}
} else {
content
.introspectTableView { tableView in
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20))
}
}
}
}

View file

@ -54,6 +54,7 @@ struct SearchResultsView: View {
} }
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.inlinedList()
.overlay { .overlay {
if scrapingModel.searchResults.isEmpty { if scrapingModel.searchResults.isEmpty {
if navModel.showSearchProgress { if navModel.showSearchProgress {

View file

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Introspect
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
@ -21,7 +22,7 @@ struct SettingsView: View {
var body: some View { var body: some View {
NavView { NavView {
Form { Form {
Section(header: "Debrid services") { Section(header: InlineHeader("Debrid Services")) {
HStack { HStack {
Text("Real Debrid") Text("Real Debrid")
Spacer() Spacer()
@ -40,11 +41,11 @@ struct SettingsView: View {
} }
} }
Section(header: "Source management") { Section(header: InlineHeader("Source management")) {
NavigationLink("Source lists", destination: SettingsSourceListView()) NavigationLink("Source lists", destination: SettingsSourceListView())
} }
Section(header: "Default actions") { Section(header: InlineHeader("Default actions")) {
if debridManager.realDebridEnabled { if debridManager.realDebridEnabled {
NavigationLink( NavigationLink(
destination: DebridActionPickerView(), destination: DebridActionPickerView(),
@ -94,14 +95,14 @@ struct SettingsView: View {
) )
} }
Section(header: Text("Updates")) { Section(header: InlineHeader("Updates")) {
Toggle(isOn: $autoUpdateNotifs) { Toggle(isOn: $autoUpdateNotifs) {
Text("Show update alerts") Text("Show update alerts")
} }
NavigationLink("Version history", destination: SettingsAppVersionView()) NavigationLink("Version history", destination: SettingsAppVersionView())
} }
Section(header: Text("Information")) { Section(header: InlineHeader("Information")) {
ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri") ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri")
ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues") ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues")
NavigationLink("About", destination: AboutView()) NavigationLink("About", destination: AboutView())

View file

@ -29,6 +29,7 @@ struct MagnetActionPickerView: View {
} }
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.inlinedList()
.navigationTitle("Default magnet action") .navigationTitle("Default magnet action")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
@ -67,6 +68,7 @@ struct DebridActionPickerView: View {
} }
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.inlinedList()
.navigationTitle("Default debrid action") .navigationTitle("Default debrid action")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }

View file

@ -21,7 +21,7 @@ struct SettingsAppVersionView: View {
ProgressView() ProgressView()
} else if !releases.isEmpty { } else if !releases.isEmpty {
List { List {
Section(header: Text("GitHub links")) { Section(header: InlineHeader("GitHub links")) {
ForEach(releases, id: \.self) { release in ForEach(releases, id: \.self) { release in
ListRowLinkView(text: release.tagName, link: release.htmlUrl) ListRowLinkView(text: release.tagName, link: release.htmlUrl)
} }

View file

@ -66,6 +66,7 @@ struct SettingsSourceListView: View {
} }
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.inlinedList()
.sheet(isPresented: $presentSourceSheet) { .sheet(isPresented: $presentSourceSheet) {
if #available(iOS 16, *) { if #available(iOS 16, *) {
SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "")

View file

@ -24,12 +24,10 @@ struct SourceListEditorView: View {
var body: some View { var body: some View {
NavView { NavView {
Form { Form {
Section { TextField("Enter URL", text: $sourceUrl)
TextField("Enter URL", text: $sourceUrl) .disableAutocorrection(true)
.disableAutocorrection(true) .keyboardType(.URL)
.keyboardType(.URL) .autocapitalization(.none)
.autocapitalization(.none)
}
} }
.onAppear { .onAppear {
sourceUrl = navModel.selectedSourceList?.urlString ?? "" sourceUrl = navModel.selectedSourceList?.urlString ?? ""

View file

@ -16,7 +16,7 @@ struct SourceSettingsView: View {
NavView { NavView {
List { List {
if let selectedSource = navModel.selectedSource { if let selectedSource = navModel.selectedSource {
Section(header: "Info") { Section(header: InlineHeader("Info")) {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
HStack { HStack {
Text(selectedSource.name) Text(selectedSource.name)
@ -78,7 +78,7 @@ struct SourceSettingsBaseUrlView: View {
@State private var tempBaseUrl: String = "" @State private var tempBaseUrl: String = ""
var body: some View { var body: some View {
Section( Section(
header: Text("Base URL"), header: InlineHeader("Base URL"),
footer: Text("Enter the base URL of your server.") footer: Text("Enter the base URL of your server.")
) { ) {
TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in
@ -110,7 +110,7 @@ struct SourceSettingsApiView: View {
var body: some View { var body: some View {
Section( Section(
header: Text("API credentials"), header: InlineHeader("API credentials"),
footer: Text("Grab the required API credentials from the website. A client secret can be an API token.") footer: Text("Grab the required API credentials from the website. A client secret can be an API token.")
) { ) {
if let clientId = selectedSourceApi.clientId, clientId.dynamic { if let clientId = selectedSourceApi.clientId, clientId.dynamic {
@ -146,7 +146,7 @@ struct SourceSettingsMethodView: View {
@ObservedObject var selectedSource: Source @ObservedObject var selectedSource: Source
var body: some View { var body: some View {
Section(header: Text("Fetch method")) { Section(header: InlineHeader("Fetch method")) {
if selectedSource.api != nil, selectedSource.jsonParser != nil { if selectedSource.api != nil, selectedSource.jsonParser != nil {
Button { Button {
selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue

View file

@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import SwiftUIX import SwiftUIX
import Introspect
struct SourcesView: View { struct SourcesView: View {
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@ -65,7 +66,7 @@ struct SourcesView: View {
} else { } else {
List { List {
if !filteredUpdatedSources.isEmpty { if !filteredUpdatedSources.isEmpty {
Section(header: "Updates") { Section(header: InlineHeader("Updates")) {
ForEach(filteredUpdatedSources, id: \.self) { source in ForEach(filteredUpdatedSources, id: \.self) { source in
SourceUpdateButtonView(updatedSource: source) SourceUpdateButtonView(updatedSource: source)
} }
@ -73,7 +74,7 @@ struct SourcesView: View {
} }
if !sources.isEmpty { if !sources.isEmpty {
Section(header: "Installed") { Section(header: InlineHeader("Installed")) {
ForEach(sources, id: \.self) { source in ForEach(sources, id: \.self) { source in
InstalledSourceView(installedSource: source) InstalledSourceView(installedSource: source)
} }
@ -89,7 +90,7 @@ struct SourcesView: View {
} }
) )
}) { }) {
Section(header: "Catalog") { Section(header: InlineHeader("Catalog")) {
ForEach(filteredAvailableSources, id: \.self) { availableSource in ForEach(filteredAvailableSources, id: \.self) { availableSource in
if !sources.contains( if !sources.contains(
where: { where: {
@ -104,6 +105,7 @@ struct SourcesView: View {
} }
} }
} }
.conditionalId(UUID())
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
} }
} }