Ferrite: Fix iOS 14 onAppear bug

onAppear does not fire properly on iOS 14 due to a longstanding
bug in SwiftUI. Add a UIKit onAppear hook for listening to these
events and implement inside the backport namespace.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-02-08 14:40:04 -05:00
parent 4512318e8f
commit 9ff7f5a7d5
23 changed files with 126 additions and 32 deletions

View file

@ -46,8 +46,8 @@
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */; }; 0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */; };
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; }; 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */; }; 0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; };
0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */; }; 0C572D4E299403B7003EEC05 /* ViewDidAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */; };
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; }; 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; }; 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
@ -169,8 +169,8 @@
0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnAppearHandler.swift; sourceTree = "<group>"; }; 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = "<group>"; };
0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DidAppearModifier.swift; sourceTree = "<group>"; }; 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearModifier.swift; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; }; 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
@ -380,7 +380,7 @@
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
0CB6516428C5A5D700DCA721 /* InlinedList.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */, 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */,
); );
path = Modifiers; path = Modifiers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -531,7 +531,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0CA148CE288903F000DE2211 /* WebView.swift */, 0CA148CE288903F000DE2211 /* WebView.swift */,
0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */, 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */,
); );
path = RepresentableViews; path = RepresentableViews;
sourceTree = "<group>"; sourceTree = "<group>";
@ -745,7 +745,7 @@
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */, 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */, 0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */, 0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */,
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
@ -795,7 +795,7 @@
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */, 0C572D4E299403B7003EEC05 /* ViewDidAppearModifier.swift in Sources */,
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,

View file

@ -56,4 +56,8 @@ extension View {
func inlinedList() -> some View { func inlinedList() -> some View {
modifier(InlinedList()) modifier(InlinedList())
} }
func viewDidAppear(_ callback: @escaping () -> Void) -> some View {
modifier(ViewDidAppearModifier(callback: callback))
}
} }

View file

@ -21,7 +21,7 @@ struct FerriteApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView() MainView()
.onAppear { .backport.onAppear {
scrapingModel.toastModel = toastModel scrapingModel.toastModel = toastModel
debridManager.toastModel = toastModel debridManager.toastModel = toastModel
pluginManager.toastModel = toastModel pluginManager.toastModel = toastModel

View file

@ -7,8 +7,9 @@
import Foundation import Foundation
// Version is optional until v1 is phased out
public struct Backup: Codable { public struct Backup: Codable {
let version: Int let version: Int?
var bookmarks: [BookmarkJson]? var bookmarks: [BookmarkJson]?
var history: [HistoryJson]? var history: [HistoryJson]?
var sourceNames: [String]? var sourceNames: [String]?

View file

@ -161,8 +161,10 @@ public class BackupManager: ObservableObject {
} }
} }
if let storedLists = backup.sourceLists, (backup.version == 1) { let version = backup.version ?? -1
// Only present in v1 backups
if let storedLists = backup.sourceLists, version < 2 {
// Only present in v1 or no version backups
for list in storedLists { for list in storedLists {
try await pluginManager.addPluginList(list.urlString, existingPluginList: nil) try await pluginManager.addPluginList(list.urlString, existingPluginList: nil)
} }

View file

@ -81,7 +81,11 @@ public class PluginManager: ObservableObject {
} }
} }
} catch { } catch {
toastModel?.updateToastDescription("Plugin fetch error: \(error)") let error = error as NSError
if error.code != -999 {
toastModel?.updateToastDescription("Plugin fetch error: \(error)")
}
print("Plugin fetch error: \(error)") print("Plugin fetch error: \(error)")
} }
} }

View file

@ -20,7 +20,12 @@ extension View {
} }
extension Backport where Content: View { extension Backport where Content: View {
@ViewBuilder func alert(isPresented: Binding<Bool>, title: String, message: String?, buttons: [AlertButton] = []) -> some View { @ViewBuilder func alert(
isPresented: Binding<Bool>,
title: String,
message: String?,
buttons: [AlertButton] = []
) -> some View {
if #available(iOS 15, *) { if #available(iOS 15, *) {
content content
.alert( .alert(
@ -63,7 +68,11 @@ extension Backport where Content: View {
} }
} }
@ViewBuilder func confirmationDialog(isPresented: Binding<Bool>, title: String, message: String?, buttons: [AlertButton]) -> some View { @ViewBuilder func confirmationDialog(
isPresented: Binding<Bool>,
title: String, message: String?,
buttons: [AlertButton]
) -> some View {
if #available(iOS 15, *) { if #available(iOS 15, *) {
content content
.confirmationDialog( .confirmationDialog(
@ -100,4 +109,18 @@ extension Backport where Content: View {
.accentColor(color) .accentColor(color)
} }
} }
@ViewBuilder func onAppear(callback: @escaping () -> Void) -> some View {
if #available(iOS 15, *) {
content
.onAppear {
callback()
}
} else {
content
.viewDidAppear {
callback()
}
}
}
} }

View file

@ -25,7 +25,7 @@ struct IndeterminateProgressView: View {
.offset(x: -reader.size.width * 0.6, y: 0) .offset(x: -reader.size.width * 0.6, y: 0)
.offset(x: reader.size.width * 1.2 * self.offset, y: 0) .offset(x: reader.size.width * 1.2 * self.offset, y: 0)
.animation(.default.repeatForever().speed(0.5), value: self.offset) .animation(.default.repeatForever().speed(0.5), value: self.offset)
.onAppear { .backport.onAppear {
withAnimation { withAnimation {
self.offset = 1 self.offset = 1
} }

View file

@ -0,0 +1,17 @@
//
// ViewDidAppearModifier.swift
// Ferrite
//
// Created by Brian Dashore on 2/8/23.
//
import SwiftUI
struct ViewDidAppearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(ViewDidAppearHandler(callback: callback))
}
}

View file

@ -54,7 +54,7 @@ struct BookmarksView: View {
} }
.inlinedList() .inlinedList()
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.onAppear { .backport.onAppear {
if debridManager.enabledDebrids.count > 0 { if debridManager.enabledDebrids.count > 0 {
viewTask = Task { viewTask = Task {
let magnets = bookmarks.compactMap { let magnets = bookmarks.compactMap {
@ -72,7 +72,7 @@ struct BookmarksView: View {
viewTask?.cancel() viewTask?.cancel()
} }
} }
.onAppear { .backport.onAppear {
applyPredicate() applyPredicate()
} }
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in

View file

@ -80,7 +80,7 @@ struct AllDebridCloudView: View {
} }
} }
} }
.onAppear { .backport.onAppear {
viewTask = Task { viewTask = Task {
await debridManager.fetchAdCloud() await debridManager.fetchAdCloud()
} }

View file

@ -55,7 +55,7 @@ struct PremiumizeCloudView: View {
} }
} }
} }
.onAppear { .backport.onAppear {
viewTask = Task { viewTask = Task {
await debridManager.fetchPmCloud() await debridManager.fetchPmCloud()
} }

View file

@ -117,7 +117,7 @@ struct RealDebridCloudView: View {
} }
} }
} }
.onAppear { .backport.onAppear {
viewTask = Task { viewTask = Task {
await debridManager.fetchRdCloud() await debridManager.fetchRdCloud()
} }

View file

@ -27,7 +27,7 @@ struct HistoryView: View {
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
} }
.onAppear { .backport.onAppear {
applyPredicate() applyPredicate()
} }
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in

View file

@ -64,7 +64,7 @@ struct PluginListView<P: Plugin, PJ: PluginJson>: View {
.environmentObject(navModel) .environmentObject(navModel)
} }
} }
.onAppear { .backport.onAppear {
filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText)
filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText)
} }

View file

@ -97,7 +97,7 @@ struct SourceSettingsBaseUrlView: View {
} }
}) })
.keyboardType(.URL) .keyboardType(.URL)
.onAppear { .backport.onAppear {
tempBaseUrl = selectedSource.baseUrl ?? "" tempBaseUrl = selectedSource.baseUrl ?? ""
} }
} }
@ -127,7 +127,7 @@ struct SourceSettingsApiView: View {
} }
}) })
.autocapitalization(.none) .autocapitalization(.none)
.onAppear { .backport.onAppear {
tempClientId = clientId.value ?? "" tempClientId = clientId.value ?? ""
} }
} }
@ -140,7 +140,7 @@ struct SourceSettingsApiView: View {
} }
}) })
.autocapitalization(.none) .autocapitalization(.none)
.onAppear { .backport.onAppear {
tempClientSecret = clientSecret.value ?? "" tempClientSecret = clientSecret.value ?? ""
} }
} }

View file

@ -138,7 +138,7 @@ struct SearchResultButtonView: View {
existingBookmark = nil existingBookmark = nil
} }
} }
.onAppear { .backport.onAppear {
// Only run a exists request if a bookmark isn't passed to the view // Only run a exists request if a bookmark isn't passed to the view
if existingBookmark == nil, !runOnce { if existingBookmark == nil, !runOnce {
let bookmarkRequest = Bookmark.fetchRequest() let bookmarkRequest = Bookmark.fetchRequest()

View file

@ -48,7 +48,7 @@ struct BackupsView: View {
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
} }
} }
.onAppear { .backport.onAppear {
backupManager.backupUrls = FileManager.default.appDirectory backupManager.backupUrls = FileManager.default.appDirectory
.appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded .appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded
} }

View file

@ -32,7 +32,7 @@ struct PluginListEditorView: View {
.autocapitalization(.none) .autocapitalization(.none)
.conditionalId(sourceUrlSet) .conditionalId(sourceUrlSet)
} }
.onAppear { .backport.onAppear {
pluginListUrl = selectedPluginList?.urlString ?? "" pluginListUrl = selectedPluginList?.urlString ?? ""
sourceUrlSet = true sourceUrlSet = true
} }

View file

@ -30,7 +30,7 @@ struct SettingsAppVersionView: View {
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
} }
} }
.onAppear { .backport.onAppear {
viewTask = Task { viewTask = Task {
do { do {
if let fetchedReleases = try await Github().fetchReleases() { if let fetchedReleases = try await Github().fetchReleases() {

View file

@ -72,7 +72,7 @@ struct MainView: View {
} }
} }
} }
.onAppear { .backport.onAppear {
if autoUpdateNotifs { if autoUpdateNotifs {
viewTask = Task { viewTask = Task {
do { do {

View file

@ -75,7 +75,7 @@ struct PluginsView: View {
ProgressView() ProgressView()
} }
} }
.onAppear { .backport.onAppear {
viewTask = Task { viewTask = Task {
await pluginManager.fetchPluginsFromUrl() await pluginManager.fetchPluginsFromUrl()
checkedForPlugins = true checkedForPlugins = true

View file

@ -0,0 +1,43 @@
//
// ViewDidAppearHandler.swift
// Ferrite
//
// Created by Brian Dashore on 2/8/23.
//
// UIKit onAppear hook to fix onAppear behavior in iOS 14
//
import SwiftUI
struct ViewDidAppearHandler: UIViewControllerRepresentable {
let callback: () -> Void
class Coordinator: UIViewController {
let callback: () -> Void
init(callback: @escaping () -> Void) {
self.callback = callback
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
callback()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(callback: callback)
}
func makeUIViewController(context: Context) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}