Skip to content

Instantly share code, notes, and snippets.

@Stmol
Forked from rileytestut/ExportIPA.swift
Created December 26, 2021 15:01
Show Gist options
  • Save Stmol/dc60f1b8765ea77eff75f4f9d03dec6f to your computer and use it in GitHub Desktop.
Save Stmol/dc60f1b8765ea77eff75f4f9d03dec6f to your computer and use it in GitHub Desktop.

Revisions

  1. @rileytestut rileytestut renamed this gist Dec 20, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ContentView.swift → ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -31,7 +31,7 @@ struct IPAFile: FileDocument
    }
    }

    struct ContentView: View
    struct ExportView: View
    {
    @State
    private var isExporting = false
  2. @rileytestut rileytestut created this gist Dec 20, 2021.
    72 changes: 72 additions & 0 deletions ContentView.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,72 @@
    // Example exportIPA() usage

    import SwiftUI
    import UniformTypeIdentifiers

    extension UTType
    {
    static let ipa = UTType(filenameExtension: "ipa")!
    }

    struct IPAFile: FileDocument
    {
    let file: FileWrapper

    static var readableContentTypes: [UTType] { [.ipa] }
    static var writableContentTypes: [UTType] { [.ipa] }

    init(ipaURL: URL) throws
    {
    self.file = try FileWrapper(url: ipaURL, options: .immediate)
    }

    init(configuration: ReadConfiguration) throws
    {
    self.file = configuration.file
    }

    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper
    {
    return self.file
    }
    }

    struct ContentView: View
    {
    @State
    private var isExporting = false

    @State
    private var ipaFile: IPAFile?

    var body: some View {
    VStack(spacing: 25) {
    Button(action: export) {
    Label("Export IPA", systemImage: "square.and.arrow.up")
    .imageScale(.large)
    .foregroundColor(.accentColor)
    }
    }
    .fileExporter(isPresented: self.$isExporting, document: self.ipaFile, contentType: .ipa) { result in
    print("Exported IPA:", result)
    }
    }

    private func export()
    {
    Task { @MainActor in
    do
    {
    let ipaPath = try await exportIPA()
    let ipaURL = URL(fileURLWithPath: ipaPath)

    self.ipaFile = try IPAFile(ipaURL: ipaURL)
    self.isExporting = true
    }
    catch
    {
    print("Could not export .ipa:", error)
    }
    }
    }
    }
    63 changes: 63 additions & 0 deletions ExportIPA.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,63 @@
    import Foundation

    // Export running app as .ipa, then return path to exported file.
    // Returns String because app crashes when returning URL from async function for some reason...
    func exportIPA() async throws -> String
    {
    // Path to app bundle
    let bundleURL = Bundle.main.bundleURL

    // Create Payload/ directory
    let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
    let payloadDirectory = temporaryDirectory.appendingPathComponent("Payload")
    try FileManager.default.createDirectory(at: payloadDirectory, withIntermediateDirectories: true, attributes: nil)

    defer {
    // Remove temporary directory
    try? FileManager.default.removeItem(at: temporaryDirectory)
    }

    let appName = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? "App"
    let appURL = payloadDirectory.appendingPathComponent("\(appName).app")
    let ipaURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(appName).ipa")

    // Copy app bundle to Payload/
    try FileManager.default.copyItem(at: bundleURL, to: appURL)

    // Remove occurrences of "swift-playgrounds-" from bundle identifier.
    // (Apple forbids registering App IDs containing that string)
    let bundleID = Bundle.main.bundleIdentifier!
    let updatedBundleID = bundleID.replacingOccurrences(of: "swift-playgrounds-", with: "")

    // Update bundle identifier
    let plistURL = appURL.appendingPathComponent("Info.plist")
    let infoPlist = try NSMutableDictionary(contentsOf: plistURL, error: ())
    infoPlist[kCFBundleIdentifierKey as String] = updatedBundleID
    try infoPlist.write(to: plistURL)

    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
    // Coordinating read access to Payload/ "forUploading" automatically zips the directory for us.
    let readIntent = NSFileAccessIntent.readingIntent(with: payloadDirectory, options: .forUploading)

    let fileCoordinator = NSFileCoordinator()
    fileCoordinator.coordinate(with: [readIntent], queue: .main) { error in
    do
    {
    guard error == nil else { throw error! }

    // Change file extension from "zip" to "ipa"
    _ = try FileManager.default.replaceItemAt(ipaURL, withItemAt: readIntent.url)

    print("Exported .ipa:", ipaURL)
    continuation.resume()
    }
    catch
    {
    print("Failed to export .ipa:", error)
    continuation.resume(throwing: error)
    }
    }
    }

    return ipaURL.path
    }