← Back to list

Why You Should Not Copy-Paste Apple Sample Code

I’ve been there. You find Apple’s sample code, it looks perfect, and you think “this is exactly what I need!” But trust me — copy-pasting…

Fahim Jatmiko · 2025-12-06 07:04 · 0 claps · 14.7 min read
#ios #maintainable-code #solid-principles #copypaste #scalable-applications
Open on Medium ↗

Why You Should Not Copy-Paste Apple Sample Code

I’ve been there. You find Apple’s sample code, it looks perfect, and you think “this is exactly what I need!” But trust me — copy-pasting it will come back to bite you. Let me tell you why.

Demo code gets you started. Engineering takes you across the finish line. (This image is AI-generated)

Demo code gets you started. Engineering takes you across the finish line. (This image is AI-generated)

Overview

With AI becoming increasingly popular, you might be excited to add an AI-powered feature to your app — say, speech-to-text. You’re googling around, and boom — you find Apple’s official sample code: Bringing advanced speech-to-text capabilities to your app.

It looks perfect. It does exactly what you need, uses the latest APIs, and even has a working UI. The temptation hits hard: just copy the code, tweak a few things, and ship it. I mean, Apple’s code must be good, right? It must be production-ready.

Not quite.

I’ve found that Apple’s sample code is rarely intended to be used directly in production. It is great for showcasing new frameworks or concepts at their WWDC, but the code is not designed to handle edge cases or long-term maintenance. In this article, I’d like to demonstrate the effects of blindly copying and pasting code from the internet into your codebase, using the Apple sample code as case study.

The Sample Code: What It Looks Like

Download Apple’s sample code Bringing advanced speech-to-text capabilities to your app and look at TranscriptView:

struct TranscriptView: View {
    @Binding var story: Story
    @State var isRecording = false
    @State var isPlaying = false
    @State var recorder: Recorder
    @State var speechTranscriber: SpokenWordTranscriber
    @State var downloadProgress = 0.0
    @State var currentPlaybackTime = 0.0
    @State var timer: Timer?

    init(story: Binding<Story>) {
        self._story = story
        let transcriber = SpokenWordTranscriber(story: story)
        recorder = Recorder(transcriber: transcriber, story: story)
        speechTranscriber = transcriber
    }
    var body: some View {
        VStack(alignment: .leading) {
            // UI content for displaying transcript
            if !story.isDone {
                Text(speechTranscriber.finalizedTranscript + speechTranscriber.volatileTranscript)
            } else {
                // Playback view
            }
        }
        .onChange(of: isRecording) { oldValue, newValue in
            guard newValue != oldValue else { return }
            if newValue == true {
                Task {
                    do {
                        try await recorder.record()
                    } catch {
                        print("could not record: \(error)")
                    }
                }
            } else {
                Task {
                    do {
                        try await recorder.stopRecording()
                    } catch {
                        print("could not stop recording: \(error)")
                    }
                }
            }
        }
    }
}

At first glance, this looks functional. But here’s what I discovered after using code like this: hidden beneath this seemingly simple code are three problems that will haunt you as your app grows. And they compound on each other in ways that’ll make you want to rewrite everything.

The Three Problems That Will Haunt You

These problems will compound as your app grows:

  1. Single Responsibility Principle Violation — Your views and collaborators are doing too much
  2. Tight Coupling and Dependency Lock-in — No interchangeable implementations
  3. Missing Production Concerns — No error handling, testing, or scalability considerations

Let’s examine each one.

Problem 1: Violation of Single Responsibility Principle

You are building a house, but one person is doing all the plumbing, electrical, and carpentry for you. Sounds like a terrible disaster waiting to happen, right? And that is like what TranscriptView is doing.

What is the Single Responsibility Principle?

The Single Responsibility Principle (SRP) is one of the fundamental principles of good software design. It states that a class, struct, or module should have only one reason to change — meaning it should have only one job or responsibility. When each class has a single responsibility, your code becomes:

  • Easier to understand: You know exactly what each class does
  • Easier to test: You can test each piece in isolation
  • Easier to modify: Changes to one feature don’t break others
  • Easier to reuse: You can use the same logic in different places

Here’s a simple example to illustrate:

// ✅ Good: ONE reason to change (when UI colors change)
struct ColorTheme {
    var primaryColor: Color
    var secondaryColor: Color
}

// ❌ Bad: MULTIPLE reasons to change
// - When UI colors change
// - When business logic changes  
// - When data model changes
struct TranscriptView: View {
    var primaryColor: Color  // UI concern
    func processAudio() { }  // Business logic concern
    var story: Story         // Data model concern
}

The second example violates SRP because it has multiple reasons to change, making it fragile and hard to maintain.

The Violations: TranscriptView

The TranscriptView violates SRP by handling multiple concerns:

  • UI Rendering: The primary responsibility of SwiftUI views is providing what is displayed to the screen. This includes what colors, fonts, margins to be displayed.
  • Business Logic Coordination: Orchestrates recording and playback workflows
.onChange(of: isRecording) { oldValue, newValue in
    if newValue == true {
        Task {
            try await recorder.record()
        }
    } else {
        Task {
            try await recorder.stopRecording()
        }
    }
}
  • Service Creation and Lifecycle: Creates and manages service dependencies
init(story: Binding<Story>) {
    let transcriber = SpokenWordTranscriber(story: story)
    recorder = Recorder(transcriber: transcriber, story: story)
    speechTranscriber = transcriber
}
  • State Management: Manages multiple state variables (recording, playback, progress, timers)
@State var isRecording = false
@State var isPlaying = false
@State var downloadProgress = 0.0
@State var currentPlaybackTime = 0.0
@State var timer: Timer?
  • Playback Timing Logic: Handles timer-based playback updates
func handlePlayback() {
    if isPlaying {
        recorder.playRecording()
        timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
            currentPlaybackTime = recorder.playerNode?.currentTime ?? 0.0
        }
    }
}
  • Text Highlighting Presentation Logic: Determines which text should be highlighted during playback
func shouldBeHighlighted(attributedStringRun: AttributedString.Runs.Run) -> Bool {
    guard isPlaying else { return false }
    let start = attributedStringRun.audioTimeRange?.start.seconds
    let end = attributedStringRun.audioTimeRange?.end.seconds
    // ... highlighting logic
}

Result: The view has at least 6 different reasons to change, violating SRP.

The Violations: SpokenWordTranscriber

The SpokenWordTranscriber class is even more problematic, mixing 6+ distinct responsibilities:

  • Transcription Setup/Initialization: Configures and initializes the transcription infrastructure
func setUpTranscriber() async throws {
    transcriber = SpeechTranscriber(locale: Locale.current, ...)
    analyzer = SpeechAnalyzer(modules: [transcriber])
    // ... transcription setup
}
  • Model Download and Management: Handles speech recognition model installation
func ensureModel(transcriber: SpeechTranscriber, locale: Locale) async throws {
    if await installed(locale: locale) {
        return
    } else {
        try await downloadIfNeeded(for: transcriber)
    }
}

func downloadIfNeeded(for module: SpeechTranscriber) async throws {
    if let downloader = try await AssetInventory.assetInstallationRequest(...) {
        self.downloadProgress = downloader.progress
        try await downloader.downloadAndInstall()
    }
}
  • Data Persistence: Directly updates the Story model
func updateStoryWithNewText(withFinal str: AttributedString) {
    story.text.wrappedValue.append(str)
}
  • UI Presentation Logic: Applies color formatting to text (presentation concern in business logic)
volatileTranscript = text
volatileTranscript.foregroundColor = .purple.opacity(0.4)  // UI concern!
  • Audio Format Conversion: Coordinates buffer conversion
func streamAudioToTranscriber(_ buffer: AVAudioPCMBuffer) async throws {
    let converted = try self.converter.convertBuffer(buffer, to: analyzerFormat)
    let input = AnalyzerInput(buffer: converted)
    inputBuilder.yield(input)
}
  • Progress Tracking: Manages download progress state
var downloadProgress: Progress?

Result: The transcriber class changes when:

  • Transcription APIs change
  • Model download logic changes
  • Story update mechanism changes
  • UI color requirements change
  • Audio format conversion needs change
  • Progress tracking needs change

This is violation of SRP — the class has multiple reasons to change, making it fragile and hard to maintain.

Why This Will Hurt You Later

  1. Views become unmaintainable: As you implement features (pause/resume, editing, sharing) you take 100 lines to over 500 lines of view. Finding and fixing bugs become a nightmare. When there are changes to the requirement you have to change the view, and that might kill the UI. Bugs are more difficult to find and correct. It takes longer to implement features. Code reviews become painful.
  2. Team conflicts: Multiple developers can’t work on the same view without constant merge conflicts. The view becomes a bottleneck.
  3. Automated testing becomes impossible: You can’t test business logic without instantiating SwiftUI views, which means:
  • Tests are slow (they need to render UI)
  • Tests are brittle (they break when UI changes)
  • You can’t test edge cases easily
  • You can’t test business logic in isolation
  1. Code duplication: When you need similar functionality in another view, you’ll copy-paste the logic, creating duplicate code that’s hard to maintain.

The Slippery Slope: How Features Multiply Complexity

You start with Apple’s clean, 100-line view and it performs excellently. Then product requirements kick in: editing (Month 2), sharing (Month 3), cloud sync (Month 4), search and highlighting (Month 5). Then, you add more state variables, more business logic, and more complexity to the feature. By Month 6 you’d probably have over 500 lines and are scrolling more than actually coding.

The actual sample code already has additional complexity — methods like handlePlayback(), shouldBeHighlighted(), and textScrollView() are implemented in extensions, further demonstrating how responsibilities accumulate. The example below shows what happens when you continue adding features without addressing architectural concerns.

After 6 months: The reality

While this example is hypothetical, I’ve seen similar patterns in production codebases that started with sample code. Your view now looks like this:

struct TranscriptView: View {
    // 25+ @State variables
    @State var isRecording = false
    @State var isPlaying = false
    @State var isEditing = false
    @State var isSharing = false
    @State var isSyncing = false
    @State var isSearching = false
    @State var downloadProgress = 0.0
    @State var exportProgress = 0.0
    @State var syncProgress = 0.0
    @State var currentPlaybackTime = 0.0
    @State var selectedTextRange: Range<String.Index>?
    @State var searchText = ""
    @State var searchResults: [SearchResult] = []
    @State var highlightedRanges: [Range<String.Index>] = []
    @State var editHistory: [AttributedString] = []
    @State var syncStatus: SyncStatus = .idle
    @State var hasUnsyncedChanges = false
    @State var timer: Timer?
    @State var searchTimer: Timer?
    @State var syncTimer: Timer?
    @State var recorder: Recorder
    @State var speechTranscriber: SpokenWordTranscriber
    @State var analytics: AnalyticsService?
    @State var errorTracker: ErrorTracker?
    @State var networkMonitor: NetworkMonitor?
    // ... and more ...
    // 500+ lines of business logic mixed with UI
    // 10+ async operations
    // 5+ timers
    // Error handling in 20+ places
    // Analytics calls scattered everywhere
    // Impossible to test
    // Impossible to understand
    // Impossible to modify safely
}

Why this happens:

Each new feature requires:

  • New state variables (managed in the view)
  • New business logic (written in the view)
  • New async operations (handled in the view)
  • New error handling (scattered throughout the view)
  • Integration with existing features (creating dependencies)

Without proper architecture, every feature multiplies the complexity. The view becomes a god object (a class that knows and does too much) that knows and does everything. Changing one feature risks breaking others. Testing becomes impossible. Onboarding new developers takes weeks instead of days.

This is why architecture matters: it prevents this exponential complexity growth by keeping concerns separated and logic testable.

A Note on When to Fix SRP Violations

Being able to detect SRP violations is one thing, but determining whether a violation should be fixed is yet another. It isn’t wise to apply the SRP if there are no symptoms. Needlessly splitting up classes that cause no maintainability problems can add extra complexity. The complexity that arises from fixing an SRP violation may not be worth the benefits it provides. The trick in software design is to manage complexity effectively, not to achieve perfect adherence to principles.

Deciding to fix an SRP violation can be subjective, and when working in a team, each team member may have different opinions on whether a particular violation warrants refactoring. What one developer sees as a clear violation might be acceptable to another based on their experience, the project’s context, or their tolerance for complexity. This is why code reviews and team discussions are valuable — they help align the team on when SRP violations should be addressed.

However, in the case of sample code that you’re copying into production, these violations are already problematic. The sample code is designed for demonstration, not long-term maintenance. As you add features, these violations will cause the symptoms discussed above. It’s better to address them early when the codebase is still small and manageable, rather than waiting until you’re dealing with a 500-line view that no one understands.

Problem 2: Tight Coupling and Dependency Lock-in

Here’s another trap I fell into. While Problem 1 is about what responsibilities a class has, Problem 2 is about how it creates and manages its dependencies. The view directly creates and depends on concrete classes, making it impossible to swap implementations or test properly. This is also known as the control freak anti-pattern — in which a class builds its own dependencies rather than receiving them.

The Control Freak Anti-Pattern

The view directly creates and depends on concrete classes:

@State var recorder: Recorder  // Concrete class
@State var speechTranscriber: SpokenWordTranscriber  // Concrete class

init(story: Binding<Story>) {
    let transcriber = SpokenWordTranscriber(story: story)  // Can't swap this
    recorder = Recorder(transcriber: transcriber, story: story)  // Can't swap this
}

This may be fine in small projects, but gets really tricky when a codebase expands. Also, a class should not create its own dependencies. Instead, dependencies should be created in a root component and injected through constructor injection. Here’s the difference:

// ❌ Bad: Direct dependency creation (control freak anti-pattern)
init(story: Binding<Story>) {
    let transcriber = SpokenWordTranscriber(story: story)  // Can't swap this
    recorder = Recorder(transcriber: transcriber, story: story)  // Can't swap this
}

// ✅ Better: Dependency injection
init(
    story: Binding<Story>,
    recorder: Recorder,
    transcriber: SpokenWordTranscriber
) {
    self._story = story
    self._recorder = State(initialValue: recorder)
    self._transcriber = State(initialValue: transcriber)
}

Important note: While dependency injection with concrete classes (as shown above) is better than creating dependencies internally, it still has limitations for testing and interchangeability. To achieve true testability and the ability to swap implementations, you should inject protocols instead of concrete classes. This allows you to inject mock implementations during testing or different implementations in production. The example above uses concrete classes for simplicity, but in production code, you’d typically define protocols like RecorderProtocol and TranscriberProtocol and inject those instead.

You make the most of the versatility of dependency injection: you can change implementations when something needs to be tested, or when you have to change requirements.

Why This Will Hurt You

  1. You can’t swap implementations: Want to use a different recording service? A cloud transcription API? You’re stuck. You’ll need to modify the view every time.
  2. You can’t test properly: You can’t inject mock dependencies, so you can’t:
  • Test error scenarios
  • Test with different data
  • Test offline behavior
  • Test performance with large files
  1. Platform lock-in: The code is tightly coupled to iOS-specific implementations. If you want to share logic with macOS or watchOS, you’re out of luck.
  2. Vendor lock-in: If Apple changes their APIs or you want to support multiple transcription services, you’ll need to rewrite large portions of your code.

A Real-World Scenario

Imagine it’s three months after you’ve built your app. Your business requirements have evolved, and now you need to support cloud-based transcription services in addition to the local on-device transcription. But your view is tightly coupled to the SpokenWordTranscriber implementation, which was designed specifically for local processing.

You’re stuck with two terrible options. First, you could create an entirely new view that duplicates most of your existing code but uses a cloud transcription service instead. This means code duplication, maintenance nightmares, and bugs appearing in one version but not the other. Every future feature or bug fix would need to be implemented twice.

Or, you could try to refactor your existing code to support both transcription types. But this is risky and time-consuming. You’d need to untangle the direct dependencies throughout your view hierarchy, potentially breaking existing functionality. The refactoring would require extensive testing to ensure nothing regresses, and you’d likely need to rewrite significant portions of code that were working perfectly fine before. All because the initial design didn’t account for future flexibility.

Problem 3: Missing Production Concerns

While the sample code looks terrific for demos, it lacks essential production considerations which will cause issues if it’s used in the real world.

What’s Missing

  1. Error Handling: Just prints to console — no user feedback, no recovery strategies, no graceful degradation. Take a look at these kinds of real applications: User denies microphone permission: The app crashes or shows no feedback Device runs out of storage: Recording fails silently with no user notification Network drops during cloud sync: No retry logic, data is lost Recording interrupted by phone call: No recovery mechanism, partial transcript is lost
  2. When recording fails, the user sees nothing. When transcription errors occur, the app crashes or silently fails.
  3. Testing: No testability — can’t inject dependencies or mock services. You can’t verify that your code works correctly, handles edge cases, or recovers from failures. Every change becomes a gamble.
  4. Observability: No logging, analytics, or monitoring. When something goes wrong in production, you have no way to diagnose the issue. You can’t track usage patterns, identify bottlenecks, or understand user behavior.
  5. Edge Cases: No handling of network failures, permission denials, resource constraints, or device limitations. What happens when the device runs out of storage? When the user denies microphone permission? When the app goes to background during recording?
  6. Performance: No optimization for large files or long recordings. The sample code might work fine for a 30-second demo, but what about a 2-hour interview? Memory leaks, performance degradation, and battery drain become real issues.

How These Problems Compound Over Time

These problems don’t exist in isolation. They compound on each other, and it happens faster than you think.

  1. Week 1: You copy-paste the code. It works! 🎉
  2. Month 1: You add a few features. The view grows to 200 lines. It’s getting messy, but still manageable.
  3. Month 3: You need to add error handling, analytics, and retry logic. The view is now 400 lines. Testing is becoming difficult.
  4. Month 6: Your team has grown. Multiple people are editing the same view. Merge conflicts are constant. Bugs are increasing.
  5. Month 12: The view is 800+ lines. No one understands it. Every change breaks something else. You’re spending more time fixing bugs than building features.
  6. Month 18: You realize you need to rewrite everything, but you can’t because:
  • You have customers depending on the current behavior
  • You don’t have time for a rewrite
  • The code is too complex to refactor safely

You’re stuck.

The three problems we’ve examined — SRP violations, tight coupling, and missing production concerns — don’t just add up; they multiply. What starts as a simple 100-line view becomes an 800-line monster in 12 months, and by then, it’s too late to fix it easily.

What Apple’s Sample Code Is Actually For

Let me be clear: Apple’s sample code is great. It just serves a different purpose than you might think.

Learning: Understand how to use new APIs ✅ Reference: See how APIs work together ✅ Prototyping: Quickly test ideas ✅ Documentation: See real-world usage examples

It’s not designed for:

Production apps: Missing error handling, testing, architecture ❌ Team collaboration: No clear structure or patterns ❌ Long-term maintenance: Technical debt accumulates quickly ❌ Scalability: Doesn’t handle growth well

Warning Signals When Code Needs Refactoring

Consider this checklist when evaluating sample code (or code you’ve already copied):

  • Classes or Structs create their own dependencies in init() methods
  • Views contain business logic and complex state management beyond simple UI state management
  • Error handling is missing or only uses print() statements
  • Classes have multiple responsibilities (e.g., both UI and data processing)
  • No way to inject dependencies for testing or swapping implementations
  • No separation between presentation and business logic
  • Code duplication when similar functionality is needed elsewhere

If you check 3+ items, the code needs refactoring before it goes to production.

The Right Approach: Learn, Don’t Copy

Instead of copy-pasting sample code, treat Apple’s examples as concept demonstrations, not templates. Use them to understand how APIs work — then apply that knowledge within your own architecture. Here’s a healthier workflow:

  1. Understand the concepts — Study how Apple’s code demonstrates specific APIs and patterns.
  2. Apply them intentionally — Integrate the ideas into your existing architecture instead of replacing it.
  3. Add production concerns — Incorporate error handling, testing, logging, analytics, and real-world edge cases.
  4. Refactor for maintainability — Separate responsibilities, use dependency injection, and follow SOLID principles. Architectural patterns like MVVM or TCA can help enforce these practices. While I don’t dive deep into them here, I plan to cover them in a future article about building a maintainable speech-to-text app.

Quick Wins: What You Can Do Today

Even if full refactoring is for a future article, here are immediate improvements you can make:

  1. Extract business logic: Move recording/playback logic out of the view into a separate class or ViewModel
  2. Add error handling: Replace print() statements with proper error handling and user feedback
  3. Use dependency injection: Instead of creating dependencies in init(), pass them as parameters
  4. Separate concerns: Create separate classes for transcription, recording, and playback instead of one monolithic class

These small changes won’t solve everything, but they’ll prevent the worst of the complexity explosion as you add features.

Conclusion: Invest in Architecture From Day One

Apple’s sample code is an excellent learning resource, but it’s not production-ready. Copy-pasting it will create a codebase that’s hard to test, maintain, and scale.

The three problems we’ve examined — SRP violations, tight coupling, and missing production concerns — don’t just add up; they multiply. What starts as a simple 100-line view becomes an 800-line monster in 12 months.

The key takeaway: The code that gets you to market quickly isn’t always the code that keeps you there.

Here’s what to remember:

  1. Sample Code ≠ Production Code: Apple’s samples are for learning, not production use.
  2. Architecture Matters: Without proper architecture, your codebase will become unmaintainable as it grows.
  3. Problems Compound: Small issues become big problems over time. Fix them early.
  4. Learn, Don’t Copy: Understand the concepts, then apply them to your architecture.
  5. Invest in Quality: The time you spend on architecture and patterns pays off as your app grows.

I understand that sometimes you need to ship features fast, and preventing 100% of scalability issues might not be worth the time investment — it could be over-engineering. It’s okay to manage some tech debt, especially in early stages or when validating product-market fit. The key is being intentional about it: know what debt you’re taking on, document it, and plan to address it before it compounds into unmanageable complexity.

Next Steps

If you’ve already copy-pasted sample code into your project:

  1. Audit your codebase: Identify views that create their own dependencies or mix multiple responsibilities
  2. Document technical debt: Create a list of known issues and prioritize them
  3. Plan refactoring sprints: Set aside time for refactoring before the codebase becomes unmanageable
  4. Start small: Begin with the “Quick Wins” mentioned above — they provide immediate value without requiring a full rewrite

If you’re starting a new project:

  1. Design with architecture in mind: Plan for separation of concerns from day one
  2. Use dependency injection: Make dependencies explicit and swappable
  3. Write tests early: Testable code is usually well-architected code
  4. Review sample code critically: Learn from it, but don’t copy it directly

Your future self (and your team) will thank you. Remember: Start with proper architecture — abstraction, separation of concerns, and testability — from day one. Your app’s success depends on it.


메타데이터
post_id
6075772c19f2
slug
the-hidden-costs-of-copy-pasting-sample-code-6075772c19f2
url
https://medium.com/@fahimjatmiko/the-hidden-costs-of-copy-pasting-sample-code-6075772c19f2
canonical_url
https://medium.com/@fahimjatmiko/the-hidden-costs-of-copy-pasting-sample-code-6075772c19f2
author_url
https://medium.com/@fahimjatmiko
status
ok
fetched_at
2026-07-25 17:01:00