Skip to content

Commit d0764f4

Browse files
committed
refactor: uikit tabbar
1 parent 6c0dc83 commit d0764f4

2 files changed

Lines changed: 175 additions & 133 deletions

File tree

Lines changed: 102 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -10,140 +10,17 @@ import Nuke
1010
import IDeviceSwift
1111
import OSLog
1212

13-
@main
14-
struct FeatherApp: App {
15-
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
16-
17-
let heartbeat = HeartbeatManager.shared
18-
19-
@StateObject var downloadManager = DownloadManager.shared
20-
let storage = Storage.shared
21-
22-
var body: some Scene {
23-
WindowGroup {
24-
VStack {
25-
DownloadHeaderView(downloadManager: downloadManager)
26-
.transition(.move(edge: .top).combined(with: .opacity))
27-
VariedTabbarView()
28-
.environment(\.managedObjectContext, storage.context)
29-
.onOpenURL(perform: _handleURL)
30-
.transition(.move(edge: .top).combined(with: .opacity))
31-
}
32-
.animation(.smooth, value: downloadManager.manualDownloads.description)
33-
.onReceive(NotificationCenter.default.publisher(for: .heartbeatInvalidHost)) { _ in
34-
DispatchQueue.main.async {
35-
UIAlertController.showAlertWithOk(
36-
title: "InvalidHostID",
37-
message: .localized("Your pairing file is invalid and is incompatible with your device, please import a valid pairing file.")
38-
)
39-
}
40-
}
41-
// dear god help me
42-
.onAppear {
43-
if let style = UIUserInterfaceStyle(rawValue: UserDefaults.standard.integer(forKey: "Feather.userInterfaceStyle")) {
44-
UIApplication.topViewController()?.view.window?.overrideUserInterfaceStyle = style
45-
}
46-
47-
UIApplication.topViewController()?.view.window?.tintColor = UIColor(Color(hex: UserDefaults.standard.string(forKey: "Feather.userTintColor") ?? "#848ef9"))
48-
}
49-
}
50-
}
51-
52-
private func _handleURL(_ url: URL) {
53-
if url.scheme == "feather" {
54-
/// feather://import-certificate?p12=<base64>&mobileprovision=<base64>&password=<base64>
55-
if url.host == "import-certificate" {
56-
guard
57-
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
58-
let queryItems = components.queryItems
59-
else {
60-
return
61-
}
62-
63-
func queryValue(_ name: String) -> String? {
64-
queryItems.first(where: { $0.name == name })?.value?.removingPercentEncoding
65-
}
66-
67-
guard
68-
let p12Base64 = queryValue("p12"),
69-
let provisionBase64 = queryValue("mobileprovision"),
70-
let passwordBase64 = queryValue("password"),
71-
let passwordData = Data(base64Encoded: passwordBase64),
72-
let password = String(data: passwordData, encoding: .utf8)
73-
else {
74-
return
75-
}
76-
77-
let generator = UINotificationFeedbackGenerator()
78-
generator.prepare()
79-
80-
guard
81-
let p12URL = FileManager.default.decodeAndWrite(base64: p12Base64, pathComponent: ".p12"),
82-
let provisionURL = FileManager.default.decodeAndWrite(base64: provisionBase64, pathComponent: ".mobileprovision"),
83-
FR.checkPasswordForCertificate(for: p12URL, with: password, using: provisionURL)
84-
else {
85-
generator.notificationOccurred(.error)
86-
return
87-
}
88-
89-
FR.handleCertificateFiles(
90-
p12URL: p12URL,
91-
provisionURL: provisionURL,
92-
p12Password: password
93-
) { error in
94-
if let error = error {
95-
UIAlertController.showAlertWithOk(title: .localized("Error"), message: error.localizedDescription)
96-
} else {
97-
generator.notificationOccurred(.success)
98-
}
99-
}
100-
101-
return
102-
}
103-
/// feather://export-certificate?callback_template=<template>
104-
/// ?callback_template=: This is how we callback to the application requesting the certificate, this will be a url scheme
105-
/// example: livecontainer%3A%2F%2Fcertificate%3Fcert%3D%24%28BASE64_CERT%29%26password%3D%24%28PASSWORD%29
106-
/// decoded: livecontainer://certificate?cert=$(BASE64_CERT)&password=$(PASSWORD)
107-
/// $(BASE64_CERT) and $(PASSWORD) must be presenting in the callback template so we can replace them with the proper content
108-
if url.host == "export-certificate" {
109-
guard
110-
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
111-
else {
112-
return
113-
}
114-
115-
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
116-
guard let callbackTemplate = queryItems["callback_template"]?.removingPercentEncoding else { return }
117-
118-
FR.exportCertificateAndOpenUrl(using: callbackTemplate)
119-
}
120-
/// feather://source/<url>
121-
if let fullPath = url.validatedScheme(after: "/source/") {
122-
FR.handleSource(fullPath) { }
123-
}
124-
/// feather://install/<url.ipa>
125-
if
126-
let fullPath = url.validatedScheme(after: "/install/"),
127-
let downloadURL = URL(string: fullPath)
128-
{
129-
_ = DownloadManager.shared.startDownload(from: downloadURL)
130-
}
131-
} else {
132-
if url.pathExtension == "ipa" || url.pathExtension == "tipa" {
133-
if FileManager.default.isFileFromFileProvider(at: url) {
134-
guard url.startAccessingSecurityScopedResource() else { return }
135-
FR.handlePackageFile(url) { _ in }
136-
} else {
137-
FR.handlePackageFile(url) { _ in }
138-
}
139-
140-
return
141-
}
142-
}
13+
@main enum Entry {
14+
static func main() {
15+
let delegate = AppDelegate()
16+
UIApplication.shared.delegate = delegate
17+
_ = UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
14318
}
14419
}
14520

146-
class AppDelegate: NSObject, UIApplicationDelegate {
21+
final class AppDelegate: UIResponder, UIApplicationDelegate {
22+
var window: UIWindow?
23+
14724
func application(
14825
_ application: UIApplication,
14926
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
@@ -152,6 +29,17 @@ class AppDelegate: NSObject, UIApplicationDelegate {
15229
_createDocumentsDirectories()
15330
ResetView.clearWorkCache()
15431
_addDefaultCertificates()
32+
33+
let tc = TabController()
34+
window = UIWindow(frame: UIScreen.main.bounds)
35+
window?.rootViewController = tc
36+
window?.makeKeyAndVisible()
37+
38+
DispatchQueue.main.async {
39+
self.window!.tintColor = UIColor(Color(hex: UserDefaults.standard.string(forKey: "Feather.userTintColor") ?? "#848ef9"))
40+
self.window!.overrideUserInterfaceStyle = UIUserInterfaceStyle(rawValue: UserDefaults.standard.integer(forKey: "Feather.userInterfaceStyle")) ?? .unspecified
41+
}
42+
15543
return true
15644
}
15745

@@ -164,10 +52,13 @@ class AppDelegate: NSObject, UIApplicationDelegate {
16452
config.urlCache = nil
16553
return DataLoader(configuration: config)
16654
}()
167-
let dataCache = try? DataCache(name: "thewonderofyou.Feather.datacache") // disk cache
168-
let imageCache = Nuke.ImageCache() // memory cache
55+
56+
let dataCache = try? DataCache(name: "\(Bundle.main.bundleIdentifier!).datacache")
57+
let imageCache = Nuke.ImageCache()
58+
16959
dataCache?.sizeLimit = 500 * 1024 * 1024
17060
imageCache.costLimit = 100 * 1024 * 1024
61+
17162
$0.dataCache = dataCache
17263
$0.imageCache = imageCache
17364
$0.dataLoader = dataLoader
@@ -243,5 +134,83 @@ class AppDelegate: NSObject, UIApplicationDelegate {
243134
Logger.misc.error("Failed to list signing-assets: \(error)")
244135
}
245136
}
137+
138+
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
139+
if url.scheme == "feather" {
140+
/// feather://import-certificate?p12=<base64>&mobileprovision=<base64>&password=<base64>
141+
if url.host == "import-certificate" {
142+
guard
143+
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
144+
let queryItems = components.queryItems
145+
else {
146+
return false
147+
}
148+
149+
func queryValue(_ name: String) -> String? {
150+
queryItems.first(where: { $0.name == name })?.value?.removingPercentEncoding
151+
}
152+
153+
guard
154+
let p12Base64 = queryValue("p12"),
155+
let provisionBase64 = queryValue("mobileprovision"),
156+
let passwordBase64 = queryValue("password"),
157+
let passwordData = Data(base64Encoded: passwordBase64),
158+
let password = String(data: passwordData, encoding: .utf8)
159+
else {
160+
return false
161+
}
162+
163+
let generator = UINotificationFeedbackGenerator()
164+
generator.prepare()
165+
166+
guard
167+
let p12URL = FileManager.default.decodeAndWrite(base64: p12Base64, pathComponent: ".p12"),
168+
let provisionURL = FileManager.default.decodeAndWrite(base64: provisionBase64, pathComponent: ".mobileprovision"),
169+
FR.checkPasswordForCertificate(for: p12URL, with: password, using: provisionURL)
170+
else {
171+
generator.notificationOccurred(.error)
172+
return false
173+
}
174+
175+
FR.handleCertificateFiles(
176+
p12URL: p12URL,
177+
provisionURL: provisionURL,
178+
p12Password: password
179+
) { error in
180+
if let error = error {
181+
UIAlertController.showAlertWithOk(title: .localized("Error"), message: error.localizedDescription)
182+
} else {
183+
generator.notificationOccurred(.success)
184+
}
185+
}
186+
187+
return true
188+
}
189+
/// feather://source/<url>
190+
if let fullPath = url.validatedScheme(after: "/source/") {
191+
FR.handleSource(fullPath) { }
192+
}
193+
/// feather://install/<url.ipa>
194+
if
195+
let fullPath = url.validatedScheme(after: "/install/"),
196+
let downloadURL = URL(string: fullPath)
197+
{
198+
_ = DownloadManager.shared.startDownload(from: downloadURL)
199+
}
200+
} else {
201+
if url.pathExtension == "ipa" || url.pathExtension == "tipa" {
202+
if FileManager.default.isFileFromFileProvider(at: url) {
203+
guard url.startAccessingSecurityScopedResource() else { return false }
204+
FR.handlePackageFile(url) { _ in }
205+
} else {
206+
FR.handlePackageFile(url) { _ in }
207+
}
208+
209+
return true
210+
}
211+
}
212+
213+
return false
214+
}
246215

247216
}

Feather/Views/TabController.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// TabbarController.swift
3+
// Feather
4+
//
5+
// Created by samsam on 5/8/26.
6+
//
7+
8+
import UIKit
9+
import SwiftUI
10+
11+
final class TabController: UITabBarController {
12+
override func viewDidLoad() {
13+
super.viewDidLoad()
14+
_setupTabs()
15+
}
16+
17+
private func _setupTabs() {
18+
let files = _createNavigation(
19+
with: .localized("Files"),
20+
using: UIImage(systemName: "folder.fill"),
21+
controller: UIViewController()
22+
)
23+
24+
let sources = _createNavigation(
25+
with: .localized("Sources"),
26+
using: UIImage(systemName: "globe.desk.fill"),
27+
controller: UIHostingController(rootView: SourcesView()),
28+
withNavigation: false,
29+
)
30+
31+
let library = _createNavigation(
32+
with: .localized("Apps"),
33+
using: UIImage(systemName: "square.grid.2x2.fill"),
34+
controller: UIHostingController(rootView: LibraryView()),
35+
withNavigation: false,
36+
)
37+
38+
let settings = _createNavigation(
39+
with: .localized("Settings"),
40+
using: UIImage(systemName: "gearshape.2.fill"),
41+
controller: UIHostingController(rootView: SettingsView()),
42+
withNavigation: false,
43+
)
44+
45+
self.setViewControllers([
46+
files,
47+
sources,
48+
library,
49+
settings,
50+
], animated: false)
51+
}
52+
53+
#warning("remove swiftui remnants")
54+
private func _createNavigation(
55+
with title: String,
56+
using image: UIImage?,
57+
controller: UIViewController,
58+
withNavigation: Bool = true
59+
) -> UIViewController {
60+
if withNavigation {
61+
let nav = UINavigationController(rootViewController: controller)
62+
nav.tabBarItem.title = title
63+
nav.tabBarItem.image = image
64+
nav.viewControllers.first?.navigationItem.title = title
65+
return nav
66+
} else {
67+
controller.tabBarItem.title = title
68+
controller.tabBarItem.image = image
69+
controller.navigationItem.title = title
70+
return controller
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)