Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Feature: App Bundle Explorer #322

Copy link
Copy link
@MaatheusGois

Description

@MaatheusGois
Issue body actions

📋 Feature Description

Add an App Bundle Explorer to DebugSwift that allows developers to browse app bundle contents, view embedded resources, extract and share files, search within bundle, and preview assets.

🎯 Motivation

The app bundle contains critical resources that developers often need to inspect during debugging:

  • Images and asset catalogs
  • Configuration files (plist, json, xml)
  • Localizations and strings files
  • HTML/CSS resources
  • Embedded databases
  • Certificates and provisioning profiles
  • Fonts and other assets

Currently, developers must:

  • Connect device to Mac
  • Access via Xcode's Devices window
  • Use terminal commands
  • Or access files programmatically

An in-app bundle explorer would make resource inspection immediate and intuitive.

✨ Proposed Features

Core Functionality

  • Bundle Browser: Navigate app bundle file structure
  • File Preview: View images, text, JSON, plist, etc.
  • Search: Find files by name or content
  • File Details: Size, type, permissions, dates
  • Extract/Share: Export files via share sheet
  • Asset Catalog Viewer: Browse asset catalog contents

File Viewers

  • Image Viewer: Display images with metadata
  • Text Viewer: View text files with syntax highlighting
  • JSON/Plist Viewer: Formatted, searchable view
  • PDF Viewer: View PDF documents
  • Audio/Video Player: Play media files
  • Archive Viewer: Browse .zip, .tar files
  • Certificate Viewer: Inspect certificates and profiles

Search & Analysis

  • File Search: Search by name, extension, size
  • Content Search: Search within text files
  • Size Analysis: Find large files
  • Duplicate Detection: Identify duplicate resources
  • Unused Resource Detection: Find unused assets
  • Compression Analysis: Identify optimization opportunities

Developer Tools

  • Path Copy: Copy file paths
  • Quick Look: System quick look integration
  • File Info: Detailed file metadata
  • Hex Viewer: View raw file data
  • Compare Files: Diff viewer for text files

🎨 UI/UX Design

Bundle Explorer Screen

┌─────────────────────────────────────────┐
│ App Bundle Explorer       [Search 🔍]   │
├─────────────────────────────────────────┤
│ Path: / MyApp.app                       │
│ Size: 45.2 MB                           │
├─────────────────────────────────────────┤
│ 📁 Assets.car              12.3 MB     │
│ 📁 Frameworks              18.5 MB     │
│ 📁 Localizations            2.1 MB     │
│ 📄 Info.plist               8.2 KB     │
│ 📄 embedded.mobileprovision 12.1 KB     │
│ 📁 Base.lproj               1.5 MB     │
│ 📁 en.lproj                 890 KB     │
│ 📁 pt-BR.lproj              845 KB     │
│ 🖼️ AppIcon60x60@2x.png     15.2 KB     │
│ 🖼️ LaunchImage.png         145 KB     │
│ 📄 config.json              2.3 KB     │
│ 📄 GoogleService-Info.plist 4.1 KB     │
├─────────────────────────────────────────┤
│ Quick Stats                             │
│ Total Files: 1,234                      │
│ Images: 456  |  Text: 89  |  Other: 689│
│ Largest: Frameworks/Lib.dylib (8.2MB)  │
└─────────────────────────────────────────┘

File Detail View

┌─────────────────────────────────────────┐
│ < Bundle    config.json                 │
├─────────────────────────────────────────┤
│ Preview                                  │
│ ┌─────────────────────────────────────┐ │
│ │ {                                   │ │
│ │   "apiBaseURL": "https://...",     │ │
│ │   "environment": "production",     │ │
│ │   "features": {                    │ │
│ │     "analytics": true,             │ │
│ │     "crashReporting": true         │ │
│ │   },                                │ │
│ │   "version": "1.0.0"               │ │
│ │ }                                   │ │
│ └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ File Information                         │
│ Name: config.json                       │
│ Path: /MyApp.app/config.json           │
│ Size: 2.3 KB (2,345 bytes)             │
│ Type: JSON Document                     │
│ Modified: Mar 06, 2026 10:30 AM        │
│ Permissions: -rw-r--r--                 │
├─────────────────────────────────────────┤
│ Actions                                  │
│ [Copy Path]                             │
│ [Share File]                            │
│ [View as Hex]                           │
│ [Quick Look]                            │
└─────────────────────────────────────────┘

Asset Catalog Viewer

┌─────────────────────────────────────────┐
│ < Bundle    Assets.car                  │
├─────────────────────────────────────────┤
│ Asset Catalog: 12.3 MB (456 assets)    │
├─────────────────────────────────────────┤
│ [Images] [Colors] [Data] [All]         │
├─────────────────────────────────────────┤
│ Images (234)                            │
│                                          │
│ ┌────┐  AppIcon                         │
│ │ 📱 │  60x60@2x (15.2 KB)             │
│ └────┘  60x60@3x (22.8 KB)             │
│         [Preview]                       │
│                                          │
│ ┌────┐  LaunchImage                     │
│ │ 🚀 │  375x812@2x (145 KB)            │
│ └────┘  414x896@3x (189 KB)            │
│         [Preview]                       │
│                                          │
│ ┌────┐  icon-home                       │
│ │ 🏠 │  24x24@1x (1.2 KB)              │
│ └────┘  24x24@2x (2.4 KB)              │
│         [Preview]                       │
│                                          │
│ Colors (12)                             │
│ ● AccentColor     #007AFF               │
│ ● BackgroundColor #FFFFFF               │
│ ● TextColor       #000000               │
│                                          │
│ [Load More...]                          │
└─────────────────────────────────────────┘

Search Results

┌─────────────────────────────────────────┐
│ < Search    "config"                    │
├─────────────────────────────────────────┤
│ 8 results found                         │
├─────────────────────────────────────────┤
│ Files (3)                               │
│ 📄 config.json               2.3 KB     │
│    /MyApp.app/                          │
│                                          │
│ 📄 AppConfig.plist          1.8 KB     │
│    /MyApp.app/Resources/                │
│                                          │
│ 📄 firebase-config.json     4.1 KB     │
│    /MyApp.app/                          │
├─────────────────────────────────────────┤
│ Content Matches (5)                     │
│ 📄 Info.plist                           │
│    ...UIConfigurationKey...            │
│                                          │
│ 📄 settings.json                        │
│    ..."configurationVersion": "2.0"... │
└─────────────────────────────────────────┘

🔧 Technical Implementation

Architecture

public class AppBundleExplorer {
    public static let shared = AppBundleExplorer()
    
    public func getBundleContents() -> BundleContents
    public func searchFiles(query: String) -> [BundleItem]
    public func previewFile(_ item: BundleItem) -> FilePreview?
    public func extractAssets() -> [AssetItem]
}

struct BundleContents {
    let rootPath: URL
    let items: [BundleItem]
    let totalSize: Int64
    let statistics: BundleStatistics
}

struct BundleItem: Identifiable {
    let id: UUID
    let name: String
    let path: URL
    let type: FileType
    let size: Int64
    let createdAt: Date
    let modifiedAt: Date
    let isDirectory: Bool
    
    enum FileType {
        case image, text, json, plist, binary, archive, media, other
    }
}

struct BundleStatistics {
    let totalFiles: Int
    let totalDirectories: Int
    let imageCount: Int
    let textFileCount: Int
    let largestFile: BundleItem?
    let typeBreakdown: [BundleItem.FileType: Int]
}

Bundle Enumeration

func getBundleContents(at path: URL? = nil) -> BundleContents {
    let bundlePath = path ?? Bundle.main.bundleURL
    let fileManager = FileManager.default
    
    var items: [BundleItem] = []
    var totalSize: Int64 = 0
    
    guard let enumerator = fileManager.enumerator(
        at: bundlePath,
        includingPropertiesForKeys: [
            .fileSizeKey,
            .isDirectoryKey,
            .creationDateKey,
            .contentModificationDateKey
        ]
    ) else {
        return BundleContents(
            rootPath: bundlePath,
            items: [],
            totalSize: 0,
            statistics: BundleStatistics(
                totalFiles: 0,
                totalDirectories: 0,
                imageCount: 0,
                textFileCount: 0,
                largestFile: nil,
                typeBreakdown: [:]
            )
        )
    }
    
    for case let fileURL as URL in enumerator {
        do {
            let resourceValues = try fileURL.resourceValues(
                forKeys: [
                    .fileSizeKey,
                    .isDirectoryKey,
                    .creationDateKey,
                    .contentModificationDateKey
                ]
            )
            
            let isDirectory = resourceValues.isDirectory ?? false
            let size = Int64(resourceValues.fileSize ?? 0)
            
            let item = BundleItem(
                id: UUID(),
                name: fileURL.lastPathComponent,
                path: fileURL,
                type: determineFileType(fileURL),
                size: size,
                createdAt: resourceValues.creationDate ?? Date(),
                modifiedAt: resourceValues.contentModificationDate ?? Date(),
                isDirectory: isDirectory
            )
            
            items.append(item)
            
            if !isDirectory {
                totalSize += size
            }
        } catch {
            print("Error reading file: \(error)")
        }
    }
    
    let statistics = calculateStatistics(items)
    
    return BundleContents(
        rootPath: bundlePath,
        items: items.sorted { $0.name < $1.name },
        totalSize: totalSize,
        statistics: statistics
    )
}

func determineFileType(_ url: URL) -> BundleItem.FileType {
    let ext = url.pathExtension.lowercased()
    
    switch ext {
    case "png", "jpg", "jpeg", "gif", "heic", "svg":
        return .image
    case "txt", "md", "swift", "m", "h", "c", "cpp":
        return .text
    case "json":
        return .json
    case "plist":
        return .plist
    case "zip", "tar", "gz":
        return .archive
    case "mp4", "mov", "mp3", "m4a":
        return .media
    default:
        // Check if it's a text file
        if let data = try? Data(contentsOf: url, options: .mappedIfSafe),
           String(data: data, encoding: .utf8) != nil {
            return .text
        }
        return .binary
    }
}

Asset Catalog Extraction

func extractAssets() -> [AssetItem] {
    var assets: [AssetItem] = []
    
    // Find Assets.car file
    guard let assetsURL = Bundle.main.url(
        forResource: "Assets",
        withExtension: "car"
    ) else {
        return []
    }
    
    // Use CUICatalog to read asset catalog (private API, for debug builds only)
    #if DEBUG
    if let catalog = CUICatalog(url: assetsURL) {
        for rendition in catalog.allRenditions() {
            let asset = AssetItem(
                name: rendition.name,
                type: determineAssetType(rendition),
                size: rendition.data.count,
                scale: rendition.scale,
                idiom: rendition.idiom
            )
            assets.append(asset)
        }
    }
    #endif
    
    return assets
}

struct AssetItem: Identifiable {
    let id = UUID()
    let name: String
    let type: AssetType
    let size: Int
    let scale: CGFloat
    let idiom: String
    
    enum AssetType {
        case image, color, data
    }
}

File Preview

func previewFile(_ item: BundleItem) -> FilePreview? {
    switch item.type {
    case .image:
        return previewImage(item)
    case .text, .json:
        return previewText(item)
    case .plist:
        return previewPlist(item)
    default:
        return nil
    }
}

func previewImage(_ item: BundleItem) -> FilePreview {
    guard let image = UIImage(contentsOfFile: item.path.path) else {
        return FilePreview(type: .error, content: "Failed to load image")
    }
    
    let metadata = """
    Dimensions: \(Int(image.size.width)) x \(Int(image.size.height))
    Scale: \(image.scale)x
    Orientation: \(image.imageOrientation.rawValue)
    """
    
    return FilePreview(type: .image, content: image, metadata: metadata)
}

func previewText(_ item: BundleItem) -> FilePreview {
    guard let content = try? String(contentsOf: item.path, encoding: .utf8) else {
        return FilePreview(type: .error, content: "Failed to read file")
    }
    
    return FilePreview(type: .text, content: content)
}

func previewPlist(_ item: BundleItem) -> FilePreview {
    guard let data = try? Data(contentsOf: item.path),
          let plist = try? PropertyListSerialization.propertyList(
            from: data,
            options: [],
            format: nil
          ) else {
        return FilePreview(type: .error, content: "Failed to parse plist")
    }
    
    let jsonData = try? JSONSerialization.data(withJSONObject: plist, options: .prettyPrinted)
    let jsonString = jsonData.flatMap { String(data: $0, encoding: .utf8) } ?? "Error"
    
    return FilePreview(type: .text, content: jsonString)
}

struct FilePreview {
    enum PreviewType {
        case image, text, error
    }
    
    let type: PreviewType
    let content: Any
    let metadata: String?
    
    init(type: PreviewType, content: Any, metadata: String? = nil) {
        self.type = type
        self.content = content
        self.metadata = metadata
    }
}

Search Functionality

func searchFiles(query: String, in path: URL? = nil) -> [BundleItem] {
    let contents = getBundleContents(at: path)
    let lowercaseQuery = query.lowercased()
    
    return contents.items.filter { item in
        // Search by filename
        if item.name.lowercased().contains(lowercaseQuery) {
            return true
        }
        
        // Search within text files
        if item.type == .text || item.type == .json {
            if let content = try? String(contentsOf: item.path, encoding: .utf8),
               content.lowercased().contains(lowercaseQuery) {
                return true
            }
        }
        
        return false
    }
}

📝 Implementation Checklist

Phase 1: Basic Explorer

  • Bundle file enumeration
  • Directory browsing UI
  • File details display
  • Basic file preview
  • Path navigation

Phase 2: Enhanced Preview

  • Image viewer with zoom
  • JSON/Plist formatted viewer
  • Text file viewer with syntax highlighting
  • PDF viewer
  • Quick Look integration

Phase 3: Advanced Features

  • Asset catalog extraction
  • Search functionality
  • File sharing/export
  • Size analysis
  • Hex viewer
  • File comparison

🧪 Testing Requirements

  • File enumeration accuracy
  • File type detection
  • Preview functionality for various formats
  • Search accuracy
  • Performance with large bundles
  • Asset catalog parsing

📚 Documentation

### App Bundle Explorer
- Browse app bundle file structure
- View embedded resources
- Extract and share files
- Search within bundle
- Preview assets and configurations

// Access bundle explorer
DebugSwift.Resources.showBundleExplorer()

// Search files
let results = DebugSwift.Bundle.search("config")

// Extract specific file
DebugSwift.Bundle.export(file: "config.json")

🎯 Success Criteria

  • Browse complete app bundle
  • Preview images, text, JSON, plist files
  • Search by filename and content
  • Extract and share files
  • View asset catalog contents
  • Display file metadata
  • Minimal performance impact

📊 Priority

Medium - Useful for resource inspection.

🏷️ Labels

enhancement, feature, resources, bundle, assets, medium-priority

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Backlog
    Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.