Javid
·18 min read

From Web to Native: Building iOS Apps with SelfDevKit Developer Tools

Cover Image for From Web to Native: Building iOS Apps with SelfDevKit Developer Tools

Building Native Apps with Privacy-First Tools

Moving from web development to native iOS presents unique challenges: different debugging workflows, new authentication patterns, and unfamiliar data handling. Throughout building FeelClose, a native iOS app for long-distance couples, I relied heavily on SelfDevKit to bridge these gaps. The same tools that power web development become even more valuable when working with Xcode, Swift, and mobile backends like Supabase.

Web developers transitioning to iOS development often underestimate how much their workflow depends on browser DevTools. The Network tab, console logging, and JavaScript debugging are suddenly unavailable. Instead, you're working with Xcode's debugger, print statements, and LLDB commands.

What doesn't change is the need for data transformation tools. You still need to format JSON responses, decode authentication tokens, generate UUIDs, and test regular expressions. These operations happen constantly during mobile development - the only difference is you can't just open a browser tab.

This post walks through how I use SelfDevKit while building FeelClose, a SwiftUI application that helps long-distance couples stay connected through daily rituals, nudges, and shared countdowns.

Table of contents

  1. About FeelClose
  2. The iOS development workflow
  3. JSON tools for Supabase debugging
  4. JWT decoding for authentication
  5. UUID generation for database records
  6. Base64 for image handling
  7. Unix timestamps for timezone logic
  8. Hash generation for caching
  9. URL encoding for deep links
  10. Regex testing for validation
  11. Color tools for UI design
  12. The offline advantage for mobile development
  13. Lessons from native development
  14. In summary

About FeelClose

FeelClose is a native iOS application designed specifically for long-distance relationships. The core philosophy is simple: feel close to your partner, no matter the distance.

The app provides:

  • Countdown to Next Visit - A prominent timer showing days, hours, and minutes until you're together again
  • Timezone Display - Side-by-side local times for both partners with day/night indicators
  • Nudges - Five distinct touch gestures (thinking of you, kiss, hug, miss you, love you) with custom haptic feedback patterns
  • Daily Questions - One relationship question per day at three intimacy levels (sweet, flirty, spicy)
  • Streak Tracking - Consecutive days of answering questions together with milestone celebrations
  • Good Morning/Night Greetings - Daily rituals with optional voice notes and photos
  • Home Screen Widgets - Four widgets showing countdown, partner's time, days together, and couple photos

The technical stack includes Swift 5.9, SwiftUI, iOS 17+, Supabase for the backend (PostgreSQL with real-time subscriptions), and Firebase for presence tracking and push notifications.

Building this app required constant interaction with APIs, authentication tokens, and data transformation - exactly where developer tools prove essential.

The iOS development workflow

Native iOS development differs from web development in several key ways:

Debugging API responses: In web development, the browser Network tab shows every request and response. In iOS, you're reading console output or setting breakpoints. Getting readable JSON requires extra effort.

Authentication handling: Mobile apps use JWT tokens extensively. Understanding token contents, expiration times, and claims requires decoding - which means copying tokens from logs and pasting them somewhere.

Database operations: Supabase uses UUIDs for all primary keys. Generating test UUIDs, understanding relationships, and debugging Row Level Security policies requires working with UUID formats constantly.

Asset handling: Images move between Base64, file URLs, and binary data. Profile pictures, uploaded photos, and cached images all involve encoding and decoding.

Time calculations: Long-distance apps deal heavily with timezones. Converting between Unix timestamps, ISO dates, and local times happens in every feature.

Each of these workflows benefits from having reliable tools immediately accessible - without network latency or privacy concerns.

JSON tools for Supabase debugging

Supabase returns JSON for all database operations. When building FeelClose, I constantly debug API responses to understand data structures and identify issues.

Debugging query responses

A typical Supabase query in Swift looks like:

let response = try await supabase
    .from("couples")
    .select("""
        id,
        relationship_start_date,
        next_visit_date,
        current_streak,
        users!inner(
            id,
            name,
            avatar_url,
            timezone,
            city
        )
    """)
    .eq("id", value: coupleId)
    .single()
    .execute()

print(String(data: response.data, encoding: .utf8)!)

The console output is a single line of JSON:

{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","relationship_start_date":"2024-06-15","next_visit_date":"2026-02-14T10:00:00+00:00","current_streak":47,"users":[{"id":"user-uuid-1","name":"Alex","avatar_url":"https://...","timezone":"America/New_York","city":"New York"},{"id":"user-uuid-2","name":"Jordan","avatar_url":"https://...","timezone":"Europe/London","city":"London"}]}

Using the JSON Formatter, I transform it instantly:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "relationship_start_date": "2024-06-15",
  "next_visit_date": "2026-02-14T10:00:00+00:00",
  "current_streak": 47,
  "users": [
    {
      "id": "user-uuid-1",
      "name": "Alex",
      "avatar_url": "https://...",
      "timezone": "America/New_York",
      "city": "New York"
    },
    {
      "id": "user-uuid-2",
      "name": "Jordan",
      "avatar_url": "https://...",
      "timezone": "Europe/London",
      "city": "London"
    }
  ]
}

Now I can see the nested users array structure and understand how to decode it in Swift.

Debugging Row Level Security issues

Supabase enforces Row Level Security (RLS) policies. When a query returns empty results unexpectedly, I need to understand what the database actually contains versus what the policy allows.

The RLS policy for nudges in FeelClose:

CREATE POLICY "Users can view nudges for their couple"
ON nudges FOR SELECT
USING (
  couple_id IN (
    SELECT couple_id FROM users WHERE id = auth.uid()
  )
);

When debugging, I check the actual data structure:

{
  "id": "nudge-uuid",
  "couple_id": "couple-uuid",
  "sender_id": "user-uuid",
  "type": "kiss",
  "created_at": "2026-01-31T14:30:00Z",
  "read_at": null
}

The formatted JSON makes it clear that couple_id must match the authenticated user's couple. If couple_id is null or mismatched, the policy blocks access.

For a complete guide to JSON formatting, see our JSON Formatter, Viewer & Validator Guide.

JWT decoding for authentication

Supabase authentication uses JWTs extensively. Understanding token contents is essential for debugging auth issues in FeelClose.

Understanding Supabase tokens

When a user signs in, Supabase returns an access token:

let session = try await supabase.auth.signIn(
    email: email,
    password: password
)
print(session.accessToken)

The token looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzA2NzE2ODAwLCJpYXQiOjE3MDY3MTMyMDAsImlzcyI6Imh0dHBzOi8veW91cnByb2plY3Quc3VwYWJhc2UuY28vYXV0aC92MSIsInN1YiI6ImE5YjhjN2Q2LWU1ZjQtMzIxMC1hYmNkLTk4NzY1NDMyMTBmZSIsImVtYWlsIjoiYWxleEBleGFtcGxlLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsibmFtZSI6IkFsZXgifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTcwNjcxMzIwMH1dLCJzZXNzaW9uX2lkIjoic2Vzc2lvbi11dWlkIn0.signature

Using the JWT Decoder, I see the payload:

{
  "aud": "authenticated",
  "exp": 1706716800,
  "iat": 1706713200,
  "iss": "https://yourproject.supabase.co/auth/v1",
  "sub": "a9b8c7d6-e5f4-3210-abcd-9876543210fe",
  "email": "alex@example.com",
  "app_metadata": {
    "provider": "email",
    "providers": ["email"]
  },
  "user_metadata": {
    "name": "Alex"
  },
  "role": "authenticated",
  "aal": "aal1",
  "amr": [
    {
      "method": "password",
      "timestamp": 1706713200
    }
  ],
  "session_id": "session-uuid"
}

Key insights:

  • sub is the user UUID used for RLS policies
  • exp shows token expiration (I can convert this with the timestamp tool)
  • user_metadata contains the user's name from sign-up
  • aal indicates authentication assurance level

Debugging token expiration

When users report being logged out unexpectedly, I decode their tokens to check expiration:

// Token from user's device
let tokenExp = 1706716800  // From decoded JWT

// Current time
let now = Date().timeIntervalSince1970  // 1706720000

// Token expired 3200 seconds (53 minutes) ago

The Unix Timestamp Converter helps me convert these values to human-readable dates for debugging.

Apple Sign-In identity tokens

FeelClose supports Apple Sign-In, which returns its own JWT:

func authorizationController(
    controller: ASAuthorizationController,
    didCompleteWithAuthorization authorization: ASAuthorization
) {
    if let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
       let identityToken = credential.identityToken,
       let tokenString = String(data: identityToken, encoding: .utf8) {
        // Decode this to understand what Apple provides
        print(tokenString)
    }
}

Decoding Apple's identity token reveals:

{
  "iss": "https://appleid.apple.com",
  "aud": "app.feelclose.ios",
  "exp": 1706800000,
  "iat": 1706713600,
  "sub": "001234.abcdef123456.7890",
  "email": "alex@icloud.com",
  "email_verified": "true",
  "auth_time": 1706713600,
  "nonce_supported": true
}

This helps me understand the mapping between Apple's sub claim and Supabase user IDs.

For more on JWT structure and validation, see our JWT Decoder & Validator Guide.

UUID generation for database records

Supabase uses UUIDs (Universally Unique Identifiers) for all primary keys. When building FeelClose, I work with UUIDs constantly.

Understanding UUID structure

Every record in the database has a UUID:

struct Nudge: Codable, Identifiable {
    let id: UUID           // Nudge ID
    let coupleId: UUID     // Foreign key to couples
    let senderId: UUID     // Foreign key to users
    let type: NudgeType
    let createdAt: Date
    let readAt: Date?
}

When debugging database relationships, I need to track which UUIDs connect to which records. The UUID Generator helps me:

  1. Generate test UUIDs for mock data
  2. Validate that a string is a proper UUID format
  3. Understand UUID versions (Supabase uses v4 random UUIDs)

Creating test data

During development, I create test couples and users with specific UUIDs:

// Test data setup
let testUser1 = UUID(uuidString: "11111111-1111-1111-1111-111111111111")!
let testUser2 = UUID(uuidString: "22222222-2222-2222-2222-222222222222")!
let testCouple = UUID(uuidString: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")!

The UUID generator helps me create valid UUIDs quickly and verify that my test strings are properly formatted.

Debugging foreign key relationships

When a nudge isn't appearing for the partner, I check the UUID chain:

Nudge: 98765432-abcd-efgh-ijkl-123456789012
  └─ couple_id: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
       └─ User 1: 11111111-1111-1111-1111-111111111111 (couple_id matches ✓)
       └─ User 2: 22222222-2222-2222-2222-222222222222 (couple_id matches ✓)

If any UUID in the chain is incorrect, the RLS policy blocks access.

Base64 for image handling

FeelClose handles multiple image types: profile avatars, couple photos, and photos attached to greetings. Base64 encoding is essential for debugging image uploads and displays.

Avatar upload flow

When users set their profile picture:

func uploadAvatar(image: UIImage) async throws -> URL {
    guard let imageData = image.jpegData(compressionQuality: 0.8) else {
        throw ImageError.compressionFailed
    }

    let fileName = "\(userId.uuidString)/avatar.jpg"

    try await supabase.storage
        .from("avatars")
        .upload(
            path: fileName,
            file: imageData,
            options: FileOptions(contentType: "image/jpeg")
        )

    return try supabase.storage
        .from("avatars")
        .getPublicURL(path: fileName)
}

During development, I sometimes need to verify the image data. The Base64 Image Tools let me:

  1. Encode the raw JPEG data to verify it's valid
  2. Preview the encoded image before upload
  3. Debug display issues by decoding stored Base64

Debugging image display

When images don't appear correctly, I check the Base64:

// Log the first 100 characters to verify format
let base64 = imageData.base64EncodedString()
print("Image data: \(base64.prefix(100))...")
// "/9j/4AAQSkZJRgABAQEASABIAAD/4QBMRXhpZgAA..."

A valid JPEG starts with /9j/4AAQ. If I see different characters, the image encoding failed.

Widget image caching

Home screen widgets cache partner photos locally:

struct WidgetDataManager {
    func cachePartnerPhoto(imageData: Data) {
        let base64 = imageData.base64EncodedString()
        UserDefaults(suiteName: "group.app.feelclose")?.set(
            base64,
            forKey: "partnerPhotoBase64"
        )
    }

    func loadPartnerPhoto() -> UIImage? {
        guard let base64 = UserDefaults(suiteName: "group.app.feelclose")?
            .string(forKey: "partnerPhotoBase64"),
              let data = Data(base64Encoded: base64) else {
            return nil
        }
        return UIImage(data: data)
    }
}

When widgets show placeholder images instead of the cached photo, I extract the stored Base64 and decode it with SelfDevKit to verify the data integrity.

For more on Base64 encoding, see our Base64 Encode and Decode Guide.

Unix timestamps for timezone logic

Long-distance relationships mean different timezones. FeelClose displays both partners' local times and calculates optimal call windows - all requiring extensive timestamp handling.

Timezone display calculation

The home screen shows both partners' current times:

struct TimezoneDisplayView: View {
    let myTimezone: TimeZone       // America/New_York
    let partnerTimezone: TimeZone  // Europe/London

    var body: some View {
        HStack {
            TimeDisplay(
                timezone: myTimezone,
                label: "Your Time"
            )
            TimeDisplay(
                timezone: partnerTimezone,
                label: "Their Time"
            )
        }
    }
}

When debugging timezone issues, I use the Unix Timestamp Converter to verify conversions:

Unix: 1706713200
→ UTC: 2026-01-31T15:00:00Z
→ EST (New York): 2026-01-31T10:00:00-05:00
→ GMT (London): 2026-01-31T15:00:00+00:00
→ JST (Tokyo): 2026-02-01T00:00:00+09:00

Daily question scheduling

Questions are assigned at midnight in each user's timezone:

func shouldAssignNewQuestion(for user: User) -> Bool {
    let userTimezone = TimeZone(identifier: user.timezone)!
    var calendar = Calendar.current
    calendar.timeZone = userTimezone

    let now = Date()
    let todayMidnight = calendar.startOfDay(for: now)

    // Check if we already assigned a question today
    guard let lastAssignment = user.lastQuestionDate else {
        return true
    }

    return lastAssignment < todayMidnight
}

Debugging this requires converting timestamps across timezones. If a user in Tokyo reports getting yesterday's question, I check:

User's local midnight (JST): 2026-01-31T00:00:00+09:00
As Unix timestamp: 1706626800
Last assignment timestamp: 1706540400
→ Assignment was 2026-01-30T00:00:00+09:00 (yesterday)
→ Should assign new question: true ✓

Streak grace period

Streaks have a 1-day grace period to prevent accidental breaks:

func isStreakActive(lastActivityTimestamp: TimeInterval) -> Bool {
    let now = Date().timeIntervalSince1970
    let gracePeriod: TimeInterval = 48 * 60 * 60  // 48 hours

    return now - lastActivityTimestamp < gracePeriod
}

When users report lost streaks, I compare timestamps:

Last activity: 1706540400 (2026-01-30T00:00:00)
Current time:  1706713200 (2026-01-31T15:00:00)
Difference: 172800 seconds (48 hours exactly)
→ Streak should be broken (> 48 hours grace period)

The timestamp converter helps me verify these calculations quickly.

Hash generation for caching

FeelClose uses hashes for cache keys and deduplication. The Hash Generator helps me verify implementations.

Widget refresh caching

Widgets refresh periodically but shouldn't make redundant network requests:

struct WidgetDataCache {
    func generateCacheKey(userId: UUID, dataType: String) -> String {
        let input = "\(userId.uuidString):\(dataType):\(dayIdentifier())"
        return input.sha256()
    }

    private func dayIdentifier() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter.string(from: Date())
    }
}

// Example: "a1b2c3d4-...:countdown:2026-01-31" → "3f7a2b9c..."

I use the hash generator to verify that my Swift implementation produces the same output as expected:

Input: "a1b2c3d4-e5f6-7890-abcd-ef1234567890:countdown:2026-01-31"
SHA-256: 3f7a2b9c8d1e4f0a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a

Notification deduplication

Push notifications are deduplicated using message hashes:

func shouldShowNotification(payload: [String: Any]) -> Bool {
    let payloadData = try? JSONSerialization.data(withJSONObject: payload)
    let hash = payloadData?.sha256String() ?? UUID().uuidString

    let recentHashes = UserDefaults.standard.stringArray(
        forKey: "recentNotificationHashes"
    ) ?? []

    if recentHashes.contains(hash) {
        return false  // Duplicate
    }

    // Store hash and trim to last 50
    var updated = recentHashes
    updated.append(hash)
    updated = Array(updated.suffix(50))
    UserDefaults.standard.set(updated, forKey: "recentNotificationHashes")

    return true
}

When debugging duplicate notifications, I hash the payloads to understand why deduplication failed.

FeelClose uses deep links for sharing and notifications. URL encoding ensures special characters don't break links.

Partners join using invite codes embedded in shareable links:

func generateInviteLink(code: String, referrer: String) -> URL? {
    var components = URLComponents()
    components.scheme = "https"
    components.host = "feelclose.app"
    components.path = "/join"
    components.queryItems = [
        URLQueryItem(name: "code", value: code),
        URLQueryItem(name: "ref", value: referrer)
    ]
    return components.url
}

// Result: https://feelclose.app/join?code=ABC123&ref=alex

The URL Encoder/Decoder helps me verify encoding when referrer names contain special characters:

Input: "Alex & Jordan 💕"
Encoded: "Alex%20%26%20Jordan%20%F0%9F%92%95"
Full URL: https://feelclose.app/join?code=ABC123&ref=Alex%20%26%20Jordan%20%F0%9F%92%95

Supabase storage URLs

Uploaded images have signed URLs with encoded parameters:

https://project.supabase.co/storage/v1/object/sign/avatars/user-uuid/avatar.jpg?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

When images fail to load, I decode the URL to check the token validity and path encoding.

Notifications include deep links to specific screens:

// Notification payload
{
  "aps": { "alert": "Jordan sent you a kiss 💋" },
  "deep_link": "feelclose://nudge?type=kiss&from=jordan&time=1706713200"
}

The URL decoder helps me parse these links during debugging.

Regex testing for validation

FeelClose validates user input at multiple points. The Regex Tester helps me build and test patterns.

Invite code validation

Invite codes are 6 alphanumeric characters:

func isValidInviteCode(_ code: String) -> Bool {
    let pattern = "^[A-Z0-9]{6}$"
    return code.range(of: pattern, options: .regularExpression) != nil
}

// Valid: "ABC123", "X7Y8Z9", "AAAAAA"
// Invalid: "abc123", "ABC12", "ABC-123"

I test the pattern against edge cases:

Pattern: ^[A-Z0-9]{6}$
✓ ABC123
✓ X7Y8Z9
✗ abc123 (lowercase)
✗ ABC12 (too short)
✗ ABC1234 (too long)
✗ ABC 123 (contains space)

Question answer validation

Answers have length limits and content restrictions:

func validateAnswer(_ answer: String) -> ValidationResult {
    // Must be 1-500 characters
    let lengthPattern = "^.{1,500}$"

    // Optional: Check for suspicious patterns
    let urlPattern = "https?://[^\\s]+"

    if answer.range(of: lengthPattern, options: .regularExpression) == nil {
        return .invalid("Answer must be 1-500 characters")
    }

    if answer.range(of: urlPattern, options: .regularExpression) != nil {
        return .warning("Answer contains a URL")
    }

    return .valid
}

Parsing notification payloads

APNs payloads sometimes need parsing:

// Extract nudge type from notification body
let body = "Jordan sent you a kiss 💋"
let pattern = "sent you a (\\w+)"
if let match = body.range(of: pattern, options: .regularExpression) {
    // Extract "kiss"
}

The regex tester lets me refine patterns against real notification text.

For more on regular expressions, see our Regex Tester Guide.

Color tools for UI design

FeelClose uses a coral color scheme throughout the interface. The Color Picker helped design accessible color combinations.

Brand color implementation

The primary color in SwiftUI:

extension Color {
    static let coral = Color(red: 255/255, green: 111/255, blue: 97/255)
    static let coralLight = Color(red: 255/255, green: 160/255, blue: 150/255)
    static let coralDark = Color(red: 200/255, green: 80/255, blue: 70/255)
}

// Hex equivalents
// coral: #FF6F61
// coralLight: #FFA096
// coralDark: #C85046

The color picker helps me:

  • Convert between RGB and hex formats
  • Check contrast ratios for accessibility
  • Generate complementary colors for dark mode

Streak milestone colors

Different streak lengths get different celebration colors:

func streakColor(for days: Int) -> Color {
    switch days {
    case 0..<7: return .gray
    case 7..<30: return .orange      // #FFA500
    case 30..<100: return .yellow    // #FFD700
    case 100..<365: return .purple   // #9B59B6
    default: return .coral           // #FF6F61
    }
}

Accessibility verification

Text must be readable against colored backgrounds. I verify contrast ratios:

Coral background (#FF6F61) + White text (#FFFFFF)
Contrast ratio: 3.2:1
→ Fails WCAG AA (4.5:1 required)

Coral background (#FF6F61) + Dark text (#1A1A1A)
Contrast ratio: 5.8:1
→ Passes WCAG AA ✓

The offline advantage for mobile development

Building FeelClose reinforced why offline tools matter, especially for mobile development:

Xcode workflow integration

Xcode doesn't have browser DevTools. When I need to format JSON or decode a JWT, switching to Safari adds friction. SelfDevKit runs as a native app alongside Xcode - no browser context switching required.

Sensitive token handling

Development involves real authentication tokens, API keys, and user data. I'm not comfortable pasting Supabase JWTs into online decoders - those tokens grant database access. Offline tools eliminate this security concern entirely.

No network dependency

Mobile development often happens on unreliable connections - coffee shops, flights, or just spotty office WiFi. Having tools that work instantly without network requests means consistent productivity regardless of connectivity.

Speed during debugging

When tracking down a bug, I might decode dozens of tokens, format hundreds of JSON responses, and test multiple regex patterns. Each operation taking 300ms of network latency would add up to significant wasted time. Offline tools complete in milliseconds.

For more on the privacy and performance benefits of offline tools, see Why Offline-First Developer Tools Matter More Than Ever.

Lessons from native development

Building FeelClose as a native iOS app taught me several lessons about tooling:

1. Data format tools are universal

Whether building for web or mobile, you need the same core utilities: JSON formatting, URL encoding, Base64 conversion, timestamp handling. The platform changes, but the data transformation requirements don't.

2. Authentication debugging is constant

Mobile apps use tokens extensively. Understanding JWT structure, verifying claims, and checking expiration is a daily activity. Having a reliable JWT decoder accessible at all times is essential.

3. UUID literacy matters

Working with Supabase (and most modern databases) means working with UUIDs. Generating test UUIDs, validating format, and tracking relationships becomes second nature.

4. Timezone complexity is underestimated

Any app dealing with users in different timezones needs extensive timestamp debugging. Converting between Unix timestamps, ISO dates, and localized displays is surprisingly complex.

5. Color tools save iteration

Designing accessible color schemes requires checking contrast ratios and testing combinations. Having color conversion tools available speeds up UI iteration.

In summary

Native iOS development presents unique challenges, but the core need for data transformation tools remains constant. Building FeelClose required daily use of:

The key benefits for native development:

  1. No browser required - Tools run alongside Xcode without context switching
  2. Privacy for tokens - Authentication credentials stay on your machine
  3. Instant results - No network latency during intensive debugging
  4. Consistent availability - Works regardless of connectivity

Whether you're building web services or native mobile apps, the same developer tools power your workflow. Having them available offline, instantly, and privately makes the difference between friction and flow.

Ready to streamline your iOS development workflow?

SelfDevKit includes 50+ developer tools that work entirely offline. Everything runs locally on your machine - no network requests, no data logging, no privacy concerns.

Download SelfDevKit — available for macOS, Windows, and Linux.

Or explore the full toolkit at selfdevkit.com/features to see all available tools including the JSON formatter, JWT decoder, and UUID generator.

Start building with confidence, knowing your authentication tokens and API data never leave your machine.

Related Articles

Getting Started with SelfDevKit: The Complete Guide to 50+ Offline Developer Tools
DEVELOPER TOOLS

Getting Started with SelfDevKit: The Complete Guide to 50+ Offline Developer Tools

Master SelfDevKit from installation to advanced workflows. Learn how to use JSON tools, JWT decoder, ID generators, and 50+ developer utilities to boost your productivity while keeping your data completely private.

Read →
JWT Decoder & Validator: The Complete Guide to JSON Web Tokens
DEVELOPER TOOLS

JWT Decoder & Validator: The Complete Guide to JSON Web Tokens

Learn how to decode, validate, and debug JWT tokens securely. Understand JWT structure, algorithms, claims, and why offline decoders protect your authentication secrets.

Read →
JSON Formatter, Viewer & Validator: The Complete Guide for Developers
DEVELOPER TOOLS

JSON Formatter, Viewer & Validator: The Complete Guide for Developers

Learn how to format, view, validate, and debug JSON data efficiently. Discover the best JSON tools for developers and why offline formatters protect your sensitive API data.

Read →
Building Real-World Services with SelfDevKit: A Developer Case Study
DEVELOPER TOOLS

Building Real-World Services with SelfDevKit: A Developer Case Study

Learn how I use SelfDevKit to build production services like Is It Visible, a mountain visibility forecasting platform. From JSON debugging to URL encoding, see the developer tools that power real projects.

Read →