Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 20994b7

Browse files
authored
Fix iPad sidebar issue on iOS 15+, implement async Zsign signing with safe temp workflow, add emoji-based progress HUD and full unzip → sign → zip → share flow
1 parent d379f85 commit 20994b7

1 file changed

Lines changed: 82 additions & 98 deletions

File tree

Sources/prostore/prostore.swift

Lines changed: 82 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import SwiftUI
22
import UniformTypeIdentifiers
3-
import ZIPFoundation // ZipFoundation
3+
import ZIPFoundation
44
import ZsignSwift
55

66
struct FileItem {
@@ -13,6 +13,7 @@ struct ZsignOnDeviceApp: App {
1313
var body: some Scene {
1414
WindowGroup {
1515
ContentView()
16+
.navigationViewStyle(StackNavigationViewStyle()) // Fix iPad sidebar bug
1617
}
1718
}
1819
}
@@ -23,7 +24,7 @@ struct ContentView: View {
2324
@State private var prov = FileItem()
2425
@State private var p12Password = ""
2526
@State private var isProcessing = false
26-
@State private var message = ""
27+
@State private var progressMessage = "Idle 😎"
2728
@State private var showActivity = false
2829
@State private var activityURL: URL? = nil
2930
@State private var showPickerFor: PickerKind?
@@ -68,7 +69,7 @@ struct ContentView: View {
6869
Button(action: runSign) {
6970
HStack {
7071
Spacer()
71-
if isProcessing { ProgressView() }
72+
if isProcessing { ProgressView(progressMessage) }
7273
Text("Sign IPA").bold()
7374
Spacer()
7475
}
@@ -77,10 +78,11 @@ struct ContentView: View {
7778
}
7879

7980
Section(header: Text("Status")) {
80-
Text(message).foregroundColor(.primary)
81+
Text(progressMessage).foregroundColor(.primary)
8182
}
8283
}
8384
.navigationTitle("Zsign On Device")
85+
.navigationViewStyle(StackNavigationViewStyle()) // Fix iPad sidebar issue
8486
.sheet(item: $showPickerFor, onDismiss: nil) { kind in
8587
DocumentPicker(kind: kind, onPick: { url in
8688
switch kind {
@@ -102,151 +104,133 @@ struct ContentView: View {
102104

103105
func runSign() {
104106
guard let ipaURL = ipa.url, let p12URL = p12.url, let provURL = prov.url else {
105-
message = "Pick all input files first."
107+
progressMessage = "Pick all input files first 😅"
106108
return
107109
}
110+
108111
isProcessing = true
109-
message = "Working..."
112+
progressMessage = "Preparing files 📂"
110113

111114
DispatchQueue.global(qos: .userInitiated).async {
112115
do {
113116
let fm = FileManager.default
114-
115-
// create unique temp root and separate subfolders to avoid collisions
116117
let tmpRoot = fm.temporaryDirectory.appendingPathComponent("zsign_ios_\(UUID().uuidString)")
117118
let inputs = tmpRoot.appendingPathComponent("inputs")
118119
let work = tmpRoot.appendingPathComponent("work")
119120
try fm.createDirectory(at: inputs, withIntermediateDirectories: true)
120121
try fm.createDirectory(at: work, withIntermediateDirectories: true)
121122

122-
// copy inputs into inputs/
123+
// Copy inputs
123124
let localIPA = inputs.appendingPathComponent(ipaURL.lastPathComponent)
124125
let localP12 = inputs.appendingPathComponent(p12URL.lastPathComponent)
125126
let localProv = inputs.appendingPathComponent(provURL.lastPathComponent)
126127

127-
// remove destinations if they somehow already exist (defensive)
128-
if fm.fileExists(atPath: localIPA.path) { try fm.removeItem(at: localIPA) }
129-
if fm.fileExists(atPath: localP12.path) { try fm.removeItem(at: localP12) }
130-
if fm.fileExists(atPath: localProv.path) { try fm.removeItem(at: localProv) }
128+
[localIPA, localP12, localProv].forEach { if fm.fileExists(atPath: $0.path) { try? fm.removeItem(at: $0) } }
131129

132130
try fm.copyItem(at: ipaURL, to: localIPA)
133131
try fm.copyItem(at: p12URL, to: localP12)
134132
try fm.copyItem(at: provURL, to: localProv)
135133

136-
// unzip IPA -> work/ (extract each entry to explicit destination to avoid collisions)
134+
DispatchQueue.main.async { progressMessage = "Unzipping IPA 🔓" }
135+
136+
// Unzip IPA -> work/
137137
let archive = try Archive(url: localIPA, accessMode: .read)
138138
for entry in archive {
139-
// Build destination URL under `work/` using the entry's path
140139
let dest = work.appendingPathComponent(entry.path)
141-
// Ensure parent directory exists
142140
try fm.createDirectory(at: dest.deletingLastPathComponent(), withIntermediateDirectories: true)
143141
if entry.type == .directory {
144-
// create directory
145142
try fm.createDirectory(at: dest, withIntermediateDirectories: true)
146143
} else {
147-
// extract file to exact destination
148144
try archive.extract(entry, to: dest)
149145
}
150146
}
151147

152-
// find Payload/*.app inside work/
148+
// Locate Payload/*.app
153149
let payload = work.appendingPathComponent("Payload")
154150
guard fm.fileExists(atPath: payload.path) else {
155-
throw NSError(domain: "ZsignOnDevice", code: 1, userInfo: [NSLocalizedDescriptionKey: "Payload not found in IPA"])
151+
throw NSError(domain: "ZsignOnDevice", code: 1, userInfo: [NSLocalizedDescriptionKey: "Payload not found"])
156152
}
157153
let contents = try fm.contentsOfDirectory(atPath: payload.path)
158154
guard let appName = contents.first(where: { $0.hasSuffix(".app") }) else {
159155
throw NSError(domain: "ZsignOnDevice", code: 2, userInfo: [NSLocalizedDescriptionKey: "No .app bundle in Payload"])
160156
}
161157
let appDir = payload.appendingPathComponent(appName)
162158

163-
// Call Zsign.swift package sign API
164-
DispatchQueue.main.async { message = "Signing \(appName)..." }
165-
166-
let ok = Zsign.sign(
167-
appPath: appDir.path,
168-
provisionPath: localProv.path,
169-
p12Path: localP12.path,
170-
p12Password: p12Password,
171-
entitlementsPath: "",
172-
customIdentifier: "",
173-
customName: "",
174-
customVersion: "",
175-
adhoc: false,
176-
removeProvision: false,
177-
completion: nil
178-
)
179-
180-
guard ok else {
181-
throw NSError(domain: "ZsignOnDevice", code: 3, userInfo: [NSLocalizedDescriptionKey: "Zsign.sign returned false"])
182-
}
183-
184-
// Rezip Payload -> signed IPA from the `work` folder so relative paths are correct
185-
let signedIpa = tmpRoot.appendingPathComponent("signed_\(UUID().uuidString).ipa")
186-
// ensure output folder exists (tmpRoot exists)
187-
let writeArchive = try Archive(url: signedIpa, accessMode: .create)
188-
189-
// Walk `work` and collect directories & files
190-
let enumerator = fm.enumerator(at: work, includingPropertiesForKeys: [.isDirectoryKey], options: [], errorHandler: nil)!
191-
var directories: [URL] = []
192-
var filesList: [URL] = []
193-
for case let file as URL in enumerator {
194-
// Skip the work root itself
195-
if file == work { continue }
196-
let isDirResource = try file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? file.hasDirectoryPath
197-
if isDirResource {
198-
directories.append(file)
199-
} else {
200-
filesList.append(file)
201-
}
202-
}
203-
204-
// Sort directories so parents come before children
205-
directories.sort { $0.path.count < $1.path.count }
206-
207-
// Base for relative paths is `work`
208-
let base = work
209-
210-
// Add directories first (ensure trailing slash)
211-
for dir in directories {
212-
let relative = dir.path.replacingOccurrences(of: base.path + "/", with: "")
213-
let entryPath = relative.hasSuffix("/") ? relative : relative + "/"
214-
try writeArchive.addEntry(with: entryPath, relativeTo: base, compressionMethod: .none)
215-
}
216-
217-
// Add files
218-
for file in filesList {
219-
let relative = file.path.replacingOccurrences(of: base.path + "/", with: "")
220-
try writeArchive.addEntry(with: relative, relativeTo: base, compressionMethod: .deflate)
221-
}
222-
223-
// Finalise: copy to Documents so user can share
224-
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
225-
let outURL = docs.appendingPathComponent("signed_\(UUID().uuidString).ipa")
226-
if fm.fileExists(atPath: outURL.path) { try fm.removeItem(at: outURL) }
227-
try fm.copyItem(at: signedIpa, to: outURL)
159+
DispatchQueue.main.async { progressMessage = "Signing \(appName) ✍️" }
228160

161+
// Zsign async
229162
DispatchQueue.main.async {
230-
activityURL = outURL
231-
showActivity = true
232-
message = "Done — signed IPA ready to share!"
233-
isProcessing = false
163+
Zsign.sign(
164+
appPath: appDir.relativePath,
165+
provisionPath: localProv.path,
166+
p12Path: localP12.path,
167+
p12Password: p12Password,
168+
entitlementsPath: "",
169+
removeProvision: false,
170+
completion: { _, error in
171+
DispatchQueue.main.async {
172+
if let error = error {
173+
progressMessage = "Signing failed ❌: \(error.localizedDescription)"
174+
isProcessing = false
175+
return
176+
}
177+
178+
progressMessage = "Zipping signed IPA 📦"
179+
do {
180+
let signedIpa = tmpRoot.appendingPathComponent("signed_\(UUID().uuidString).ipa")
181+
let writeArchive = try Archive(url: signedIpa, accessMode: .create)
182+
183+
let enumerator = fm.enumerator(at: work, includingPropertiesForKeys: [.isDirectoryKey], options: [], errorHandler: nil)!
184+
var directories: [URL] = []
185+
var filesList: [URL] = []
186+
for case let file as URL in enumerator {
187+
if file == work { continue }
188+
let isDir = (try? file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? file.hasDirectoryPath
189+
if isDir { directories.append(file) } else { filesList.append(file) }
190+
}
191+
directories.sort { $0.path.count < $1.path.count }
192+
let base = work
193+
for dir in directories {
194+
let relative = dir.path.replacingOccurrences(of: base.path + "/", with: "")
195+
let entryPath = relative.hasSuffix("/") ? relative : relative + "/"
196+
try writeArchive.addEntry(with: entryPath, relativeTo: base, compressionMethod: .none)
197+
}
198+
for file in filesList {
199+
let relative = file.path.replacingOccurrences(of: base.path + "/", with: "")
200+
try writeArchive.addEntry(with: relative, relativeTo: base, compressionMethod: .deflate)
201+
}
202+
203+
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
204+
let outURL = docs.appendingPathComponent("signed_\(UUID().uuidString).ipa")
205+
if fm.fileExists(atPath: outURL.path) { try fm.removeItem(at: outURL) }
206+
try fm.copyItem(at: signedIpa, to: outURL)
207+
208+
activityURL = outURL
209+
showActivity = true
210+
progressMessage = "Done! ✅ IPA ready to share 🎉"
211+
isProcessing = false
212+
213+
try? fm.removeItem(at: tmpRoot) // cleanup
214+
215+
} catch {
216+
progressMessage = "Error during zipping ❌: \(error.localizedDescription)"
217+
isProcessing = false
218+
}
219+
}
220+
}
221+
)
234222
}
235-
236-
// optional: cleanup tmp (comment out if you want to inspect)
237-
try? fm.removeItem(at: tmpRoot)
238-
239223
} catch {
240224
DispatchQueue.main.async {
241-
message = "Error: \(error.localizedDescription)"
225+
progressMessage = "Error: \(error.localizedDescription)"
242226
isProcessing = false
243227
}
244228
}
245229
}
246230
}
247231
}
248232

249-
// DocumentPicker wrapper for picking any file types
233+
// DocumentPicker wrapper
250234
struct DocumentPicker: UIViewControllerRepresentable {
251235
var kind: ContentView.PickerKind
252236
var onPick: (URL) -> Void
@@ -271,11 +255,11 @@ struct DocumentPicker: UIViewControllerRepresentable {
271255
}
272256
}
273257

258+
// ActivityView wrapper
274259
struct ActivityView: UIViewControllerRepresentable {
275260
let url: URL
276261
func makeUIViewController(context: Context) -> UIActivityViewController {
277-
let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil)
278-
return vc
262+
UIActivityViewController(activityItems: [url], applicationActivities: nil)
279263
}
280-
func updateUIViewController(_ uiActivityViewController: UIActivityViewController, context: Context) {}
264+
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
281265
}

0 commit comments

Comments
 (0)