11import SwiftUI
22import UniformTypeIdentifiers
3- import ZIPFoundation // ZipFoundation
3+ import ZIPFoundation
44import ZsignSwift
55
66struct 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
250234struct 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
274259struct 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