Skip to content

Instantly share code, notes, and snippets.

@kkebo
Forked from rileytestut/ExportIPA.swift
Last active October 24, 2025 07:11
Show Gist options
  • Select an option

  • Save kkebo/fc77cc0a2f97c4799e7a5686905d51a1 to your computer and use it in GitHub Desktop.

Select an option

Save kkebo/fc77cc0a2f97c4799e7a5686905d51a1 to your computer and use it in GitHub Desktop.

Revisions

  1. kkebo revised this gist Oct 12, 2022. 1 changed file with 10 additions and 10 deletions.
    20 changes: 10 additions & 10 deletions ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,9 @@ struct ExportView: View {
    @State private var ipaFile: URL?

    var body: some View {
    Button(action: self.export) {
    Button {
    Task { await self.export() }
    } label: {
    Label("Export IPA", systemImage: "square.and.arrow.up")
    .imageScale(.large)
    }
    @@ -19,15 +21,13 @@ struct ExportView: View {
    }
    }

    private func export() {
    Task { @MainActor in
    do {
    let url = try await exportIPA()
    self.ipaFile = url
    self.isExporting = true
    } catch {
    print("Could not export .ipa:", error)
    }
    private func export() async {
    do {
    let url = try await exportIPA()
    self.ipaFile = url
    self.isExporting = true
    } catch {
    print("Could not export .ipa:", error)
    }
    }
    }
  2. kkebo revised this gist Mar 15, 2022. 1 changed file with 3 additions and 5 deletions.
    8 changes: 3 additions & 5 deletions ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -20,13 +20,11 @@ struct ExportView: View {
    }

    private func export() {
    Task {
    Task { @MainActor in
    do {
    let url = try await exportIPA()
    await MainActor.run {
    self.ipaFile = url
    self.isExporting = true
    }
    self.ipaFile = url
    self.isExporting = true
    } catch {
    print("Could not export .ipa:", error)
    }
  3. kkebo revised this gist Dec 29, 2021. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -22,8 +22,9 @@ struct ExportView: View {
    private func export() {
    Task {
    do {
    self.ipaFile = try await exportIPA()
    let url = try await exportIPA()
    await MainActor.run {
    self.ipaFile = url
    self.isExporting = true
    }
    } catch {
  4. kkebo revised this gist Dec 24, 2021. 1 changed file with 11 additions and 14 deletions.
    25 changes: 11 additions & 14 deletions ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -1,17 +1,11 @@
    // Example exportIPA() usage

    import SwiftUI

    struct ExportView: View {
    @State private var isExporting = false
    @State private var ipaFile: URL?

    var body: some View {
    Button {
    Task {
    await self.export()
    }
    } label: {
    Button(action: self.export) {
    Label("Export IPA", systemImage: "square.and.arrow.up")
    .imageScale(.large)
    }
    @@ -25,13 +19,16 @@ struct ExportView: View {
    }
    }

    @MainActor
    private func export() async {
    do {
    self.ipaFile = try await exportIPA()
    self.isExporting = true
    } catch {
    print("Could not export .ipa:", error)
    private func export() {
    Task {
    do {
    self.ipaFile = try await exportIPA()
    await MainActor.run {
    self.isExporting = true
    }
    } catch {
    print("Could not export .ipa:", error)
    }
    }
    }
    }
  5. kkebo revised this gist Dec 21, 2021. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -28,8 +28,7 @@ struct ExportView: View {
    @MainActor
    private func export() async {
    do {
    let url = try await exportIPA()
    self.ipaFile = url
    self.ipaFile = try await exportIPA()
    self.isExporting = true
    } catch {
    print("Could not export .ipa:", error)
  6. kkebo revised this gist Dec 21, 2021. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion ExportIPA.swift
    Original file line number Diff line number Diff line change
    @@ -9,7 +9,6 @@ import var Foundation.kCFBundleIdentifierKey
    import var Foundation.kCFBundleNameKey

    // 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 -> URL {
    // Path to app bundle
    let bundleURL = Bundle.main.bundleURL
  7. kkebo revised this gist Dec 21, 2021. 2 changed files with 54 additions and 73 deletions.
    47 changes: 31 additions & 16 deletions ExportIPA.swift
    Original file line number Diff line number Diff line change
    @@ -1,16 +1,29 @@
    import Foundation
    import class Foundation.Bundle
    import class Foundation.FileManager
    import class Foundation.NSFileAccessIntent
    import class Foundation.NSFileCoordinator
    import class Foundation.NSMutableDictionary
    import struct Foundation.URL
    import struct Foundation.UUID
    import var Foundation.kCFBundleIdentifierKey
    import var Foundation.kCFBundleNameKey

    // 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
    {
    func exportIPA() async throws -> URL {
    // 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)
    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
    @@ -19,7 +32,8 @@ func exportIPA() async throws -> String

    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")
    let ipaURL = FileManager.default.temporaryDirectory
    .appendingPathComponent("\(appName).ipa")

    // Copy app bundle to Payload/
    try FileManager.default.copyItem(at: bundleURL, to: appURL)
    @@ -37,27 +51,28 @@ func exportIPA() async throws -> String

    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 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! }
    do {
    if let error = error { throw error }

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

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

    return ipaURL.path
    return ipaURL
    }
    80 changes: 23 additions & 57 deletions ExportView.swift
    Original file line number Diff line number Diff line change
    @@ -1,72 +1,38 @@
    // 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 ExportView: View
    {
    @State
    private var isExporting = false

    @State
    private var ipaFile: IPAFile?
    struct ExportView: View {
    @State private var isExporting = false
    @State private var ipaFile: URL?

    var body: some View {
    VStack(spacing: 25) {
    Button(action: export) {
    Label("Export IPA", systemImage: "square.and.arrow.up")
    .imageScale(.large)
    .foregroundColor(.accentColor)
    Button {
    Task {
    await self.export()
    }
    } label: {
    Label("Export IPA", systemImage: "square.and.arrow.up")
    .imageScale(.large)
    }
    .fileExporter(isPresented: self.$isExporting, document: self.ipaFile, contentType: .ipa) { result in
    .buttonStyle(.borderedProminent)
    .hoverEffect()
    .fileMover(
    isPresented: self.$isExporting,
    file: self.ipaFile
    ) { 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)
    }
    @MainActor
    private func export() async {
    do {
    let url = try await exportIPA()
    self.ipaFile = url
    self.isExporting = true
    } catch {
    print("Could not export .ipa:", error)
    }
    }
    }
  8. @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
  9. @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
    }