11import SwiftUI
2+ import UniformTypeIdentifiers
3+ import ZIPFoundation // ZipFoundation
4+ import Zsign // your Swift package that exports `Zsign`
5+
6+ struct FileItem {
7+ var url : URL ?
8+ var name : String { url? . lastPathComponent ?? " " }
9+ }
210
311@main
4- struct ProStore : App {
12+ struct ZsignOnDeviceApp : App {
513 var body : some Scene {
614 WindowGroup {
715 ContentView ( )
816 }
917 }
18+ }
19+
20+ struct ContentView : View {
21+ @State private var ipa = FileItem ( )
22+ @State private var p12 = FileItem ( )
23+ @State private var prov = FileItem ( )
24+ @State private var p12Password = " "
25+ @State private var isProcessing = false
26+ @State private var message = " "
27+ @State private var showActivity = false
28+ @State private var activityURL : URL ? = nil
29+ @State private var showPickerFor : PickerKind ?
30+
31+ enum PickerKind { case ipa, p12, prov }
32+
33+ var body : some View {
34+ NavigationView {
35+ Form {
36+ Section ( header: Text ( " Inputs " ) ) {
37+ HStack {
38+ Text ( " IPA: " )
39+ Spacer ( )
40+ Text ( ipa. name. isEmpty ? " none " : ipa. name) . foregroundColor ( . secondary)
41+ Button ( " Pick " ) { showPickerFor = . ipa }
42+ }
43+ HStack {
44+ Text ( " P12: " )
45+ Spacer ( )
46+ Text ( p12. name. isEmpty ? " none " : p12. name) . foregroundColor ( . secondary)
47+ Button ( " Pick " ) { showPickerFor = . p12 }
48+ }
49+ HStack {
50+ Text ( " MobileProvision: " )
51+ Spacer ( )
52+ Text ( prov. name. isEmpty ? " none " : prov. name) . foregroundColor ( . secondary)
53+ Button ( " Pick " ) { showPickerFor = . prov }
54+ }
55+ SecureField ( " P12 Password " , text: $p12Password)
56+ }
57+
58+ Section {
59+ Button ( action: runSign) {
60+ HStack {
61+ Spacer ( )
62+ if isProcessing { ProgressView ( ) }
63+ Text ( " Sign IPA " ) . bold ( )
64+ Spacer ( )
65+ }
66+ }
67+ . disabled ( isProcessing || ipa. url == nil || p12. url == nil || prov. url == nil )
68+ }
69+
70+ Section ( header: Text ( " Status " ) ) {
71+ Text ( message) . foregroundColor ( . primary)
72+ }
73+ }
74+ . navigationTitle ( " Zsign On Device " )
75+ . sheet ( item: $showPickerFor, onDismiss: nil ) { kind in
76+ DocumentPicker ( kind: kind, onPick: { url in
77+ switch kind {
78+ case . ipa: ipa. url = url
79+ case . p12: p12. url = url
80+ case . prov: prov. url = url
81+ }
82+ } )
83+ }
84+ . sheet ( isPresented: $showActivity) {
85+ if let u = activityURL {
86+ ActivityView ( url: u)
87+ } else {
88+ Text ( " No file to share " )
89+ }
90+ }
91+ }
92+ }
93+
94+ func runSign( ) {
95+ guard let ipaURL = ipa. url, let p12URL = p12. url, let provURL = prov. url else {
96+ message = " Pick all input files first. "
97+ return
98+ }
99+ isProcessing = true
100+ message = " Working... "
101+
102+ DispatchQueue . global ( qos: . userInitiated) . async {
103+ do {
104+ let fm = FileManager . default
105+ let tmp = fm. temporaryDirectory. appendingPathComponent ( " zsign_ios_ \( UUID ( ) . uuidString) " )
106+ try fm. createDirectory ( at: tmp, withIntermediateDirectories: true )
107+
108+ // copy inputs into tmp
109+ let localIPA = tmp. appendingPathComponent ( ipaURL. lastPathComponent)
110+ let localP12 = tmp. appendingPathComponent ( p12URL. lastPathComponent)
111+ let localProv = tmp. appendingPathComponent ( provURL. lastPathComponent)
112+ try fm. copyItem ( at: ipaURL, to: localIPA)
113+ try fm. copyItem ( at: p12URL, to: localP12)
114+ try fm. copyItem ( at: provURL, to: localProv)
115+
116+ // unzip IPA -> tmp
117+ let archive = try Archive ( url: localIPA, accessMode: . read)
118+ try archive. extract ( to: tmp)
119+
120+ // find Payload/*.app
121+ let payload = tmp. appendingPathComponent ( " Payload " )
122+ guard fm. fileExists ( atPath: payload. path) else {
123+ throw NSError ( domain: " ZsignOnDevice " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Payload not found " ] )
124+ }
125+ let contents = try fm. contentsOfDirectory ( atPath: payload. path)
126+ guard let appName = contents. first ( where: { $0. hasSuffix ( " .app " ) } ) else {
127+ throw NSError ( domain: " ZsignOnDevice " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " No .app bundle in Payload " ] )
128+ }
129+ let appDir = payload. appendingPathComponent ( appName)
130+
131+ // Call Zsign.swift package sign API
132+ DispatchQueue . main. async { message = " Signing \( appName) ... " }
133+
134+ // NOTE: match your Zsign API exactly. This call mirrors the wrapper you posted earlier:
135+ let ok = Zsign . sign (
136+ appPath: appDir. path,
137+ provisionPath: localProv. path,
138+ p12Path: localP12. path,
139+ p12Password: p12Password,
140+ entitlementsPath: " " , // optional
141+ customIdentifier: " " ,
142+ customName: " " ,
143+ customVersion: " " ,
144+ adhoc: false ,
145+ removeProvision: false ,
146+ completion: nil
147+ )
148+
149+ guard ok else {
150+ throw NSError ( domain: " ZsignOnDevice " , code: 3 , userInfo: [ NSLocalizedDescriptionKey: " Zsign.sign returned false " ] )
151+ }
152+
153+ // Zsign usually writes changes in-place inside the .app. Rezip Payload -> signed IPA
154+ let signedIpa = tmp. appendingPathComponent ( " signed_ \( appName) .ipa " )
155+ // create archive with Payload directory
156+ try fm. createDirectory ( at: signedIpa. deletingLastPathComponent ( ) , withIntermediateDirectories: true )
157+ let writeArchive = try Archive ( url: signedIpa, accessMode: . create)
158+
159+ // recursively add Payload
160+ let enumerator = fm. enumerator ( at: payload, includingPropertiesForKeys: nil ) !
161+ for case let file as URL in enumerator {
162+ let relative = file. path. replacingOccurrences ( of: tmp. path + " / " , with: " " )
163+ if file. hasDirectoryPath {
164+ try writeArchive. addEntry ( with: relative + " / " , type: . directory, uncompressedSize: 0 , compressionMethod: . deflate) // directories recorded
165+ } else {
166+ let data = try Data ( contentsOf: file)
167+ try writeArchive. addEntry ( with: relative, type: . file, uncompressedSize: UInt32 ( data. count) , compressionMethod: . deflate, provider: { ( position, size) -> Data in
168+ return data. subdata ( in: Int ( position) ..< Int ( position + size) )
169+ } )
170+ }
171+ }
172+
173+ // share file (move to Documents to be easily accessible)
174+ let docs = fm. urls ( for: . documentDirectory, in: . userDomainMask) . first!
175+ let outURL = docs. appendingPathComponent ( " signed_ \( UUID ( ) . uuidString) .ipa " )
176+ if fm. fileExists ( atPath: outURL. path) { try fm. removeItem ( at: outURL) }
177+ try fm. copyItem ( at: signedIpa, to: outURL)
178+
179+ DispatchQueue . main. async {
180+ activityURL = outURL
181+ showActivity = true
182+ message = " Done — signed IPA ready to share! "
183+ isProcessing = false
184+ }
185+
186+ } catch {
187+ DispatchQueue . main. async {
188+ message = " Error: \( error. localizedDescription) "
189+ isProcessing = false
190+ }
191+ }
192+ }
193+ }
194+ }
195+
196+ // DocumentPicker wrapper for picking any file types
197+ struct DocumentPicker : UIViewControllerRepresentable {
198+ var kind : ContentView . PickerKind
199+ var onPick : ( URL ) -> Void
200+
201+ func makeCoordinator( ) -> Coordinator { Coordinator ( self ) }
202+ func makeUIViewController( context: Context ) -> UIDocumentPickerViewController {
203+ let types : [ UTType ] = [ . item] // let user pick any file; could refine UTType.zip / ipa mimetype
204+ let vc = UIDocumentPickerViewController ( forOpeningContentTypes: types, asCopy: true )
205+ vc. delegate = context. coordinator
206+ vc. allowsMultipleSelection = false
207+ return vc
208+ }
209+ func updateUIViewController( _ uiViewController: UIDocumentPickerViewController , context: Context ) { }
210+
211+ class Coordinator : NSObject , UIDocumentPickerDelegate {
212+ let parent : DocumentPicker
213+ init ( _ p: DocumentPicker ) { parent = p }
214+ func documentPicker( _ controller: UIDocumentPickerViewController , didPickDocumentsAt urls: [ URL ] ) {
215+ guard let u = urls. first else { return }
216+ parent. onPick ( u)
217+ }
218+ }
219+ }
220+
221+ struct ActivityView : UIViewControllerRepresentable {
222+ let url : URL
223+ func makeUIViewController( context: Context ) -> UIActivityViewController {
224+ let vc = UIActivityViewController ( activityItems: [ url] , applicationActivities: nil )
225+ return vc
226+ }
227+ func updateUIViewController( _ uiViewController: UIActivityViewController , context: Context ) { }
10228}
0 commit comments