All articles
iOS14 min read

Swift Package Best Practices: Structure, Testing & Publishing (2025 Edition)

Level up your Swift packages with professional structure, comprehensive testing, and proper versioning. Complete guide to publishing…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

Introduction

Welcome back to our Swift Package mastery series! 🎉

If you followed **Part 1**, you’ve already experienced that magical moment — transforming your copy-paste utility code into your first working Swift package. You’ve felt the satisfaction of importing your own library and watching it work seamlessly across projects.

But here’s the thing: there’s a big difference between “it works on my machine” and “it’s production-ready.” Right now, your package might work perfectly for you, but what happens when:

**How to Build and Publish Swift Packages: Turn Your Code into Reusable Libraries (2025 Edition)** _Transform your copy-paste code into professional Swift libraries. Complete guide to package creation, testing…_medium.comhttps://medium.com/swift-pal/how-to-build-and-publish-swift-packages-turn-your-code-into-reusable-libraries-2025-edition-cb8f752e16b1

From Working to Professional

In Part 2 of our 3-part series, we’re going to transform your basic package into something you’d be proud to show in a job interview — or even publish for the world to use.

What We’re Covering Today:

Where We’re Headed:

By the end of this article, your Swift package won’t just work — it’ll be maintainable, reliable, and ready for serious use. Let’s level up! 🚀

📁 Package Structure & Best Practices

Now that your package is working, let’s talk about organizing it like a pro. Good structure makes your package easier to maintain, easier for others to contribute to, and much more professional-looking.

Organizing Your Code for Growth

Right now you probably have everything in one file, which is fine for a small package. But as your package grows, you’ll want to split things up logically:

Better File Structure:

Sources/MyUtilities/
├── Extensions/
   ├── String+Extensions.swift
   ├── Date+Extensions.swift
   └── UIColor+Extensions.swift
├── Networking/
   ├── NetworkManager.swift
   └── APIResponse.swift
├── UI/
   ├── CustomButton.swift
   └── LoadingView.swift
└── MyUtilities.swift  // Main public interface

Why This Matters:

This kind of modular thinking is exactly what makes iOS apps scalable too. If you’re interested in applying these same principles to your entire app architecture, I’ve covered this extensively in my guide on How to Structure a Scalable iOS App with Modular Architecture where I break down how to structure your project into clean modules.

**How to Structure a Scalable iOS App with Modular Architecture** _Struggling with messy codebases and slow builds? Learn how modular architecture can make your iOS app scalable…_medium.comhttps://medium.com/swift-pal/how-to-structure-a-scalable-ios-app-with-modular-architecture-b0130da83bca

The Art of Public APIs

Here’s something crucial: everything in your package is internal by default, which means it's invisible to projects that import your package. You need to be intentional about what you expose:

import Foundation

// ✅ Public - available to importing projects
public extension String {
    var trimmed: String {
        return self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    
    var isValidEmail: Bool {
        return EmailValidator.validate(self)  // Uses internal helper
    }
}
// ✅ Internal - only used within your package
struct EmailValidator {
    static func validate(_ email: String) -> Bool {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}

Golden Rule: Only make things public that you want people to actually use. Keep implementation details internal.

Documentation That Actually Helps

Good documentation separates amateur packages from professional ones. Swift has amazing built-in documentation support:

public extension String {
    /// Removes whitespace and newlines from both ends of the string
    /// 
    /// This is particularly useful for cleaning up user input or data from APIs.
    /// 
    /// Example:
    /// ```swift
    /// let userInput = "  hello world  \n"
    /// print(userInput.trimmed) // "hello world"
    /// ```
    /// 
    /// - Returns: A new string with leading and trailing whitespace removed
    var trimmed: String {
        return self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    
    /// Validates if the string is a properly formatted email address
    /// 
    /// Uses a regex pattern to check for basic email format compliance.
    /// Note: This validates format only, not whether the email actually exists.
    /// 
    /// Example:
    /// ```swift
    /// "user@example.com".isValidEmail // true
    /// "invalid-email".isValidEmail    // false
    /// ```
    /// 
    /// - Returns: `true` if the string matches email format, `false` otherwise
    var isValidEmail: Bool {
        return EmailValidator.validate(self)
    }
}

Pro tip: Hold Option and click on your documented functions in Xcode. You'll see beautiful formatted documentation just like Apple's APIs! ✨

README.md: Your Package’s Front Door

Your README is the first thing people see. Make it count:

# MyUtilities

A collection of useful Swift extensions and utilities to speed up iOS development.

## Features
- 🎯 **String Extensions**: Email validation, trimming, formatting
- 📅 **Date Extensions**: Relative time formatting, common date operations  
- 🎨 **UI Utilities**: Custom components and view helpers

## Installation

### Swift Package Manager
```swift
dependencies: [
    .package(url: "https://github.com/yourusername/MyUtilities.git", from: "1.0.0")
]
```

## Quick Start
```swift
import MyUtilities
// Clean up user input
let email = userInput.trimmed
// Validate email format
if email.isValidEmail {
    // Process valid email
}
// Get relative time
let post = Post(createdAt: someDate)
print("Posted \(post.createdAt.timeAgo)") // "Posted 2 hours ago"
```

## Requirements
- iOS 13.0+
- Xcode 12.0+
- Swift 5.3+

## License
MIT License. See LICENSE file for details.

Version Control Best Practices

Essential Files for Git:

Gitignore Essentials:

.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Testing Organization

Keep your tests organized too:

import XCTest
@testable import MyUtilities

final class StringExtensionsTests: XCTestCase {
    
    // MARK: - Trimming Tests
    func testTrimming() {
        XCTAssertEqual("  hello  ".trimmed, "hello")
        XCTAssertEqual("\nhello\n".trimmed, "hello")
        XCTAssertEqual("hello".trimmed, "hello") // No change needed
    }
    
    // MARK: - Email Validation Tests
    func testValidEmails() {
        let validEmails = [
            "test@example.com",
            "user.name@domain.co.uk",
            "user+tag@example.org"
        ]
        
        for email in validEmails {
            XCTAssertTrue(email.isValidEmail, "\(email) should be valid")
        }
    }
    
    func testInvalidEmails() {
        let invalidEmails = [
            "invalid",
            "@example.com",
            "user@",
            ""
        ]
        
        for email in invalidEmails {
            XCTAssertFalse(email.isValidEmail, "\(email) should be invalid")
        }
    }
}

Testing is absolutely crucial for packages because other developers (including future-you) will depend on your code working correctly. If you want to dive deeper into Swift testing strategies and patterns that’ll make your packages rock-solid, I’ve written a comprehensive guide that covers everything from basic unit tests to advanced testing patterns that professional iOS teams use.

The Professional Touch

Here’s what separates hobby packages from professional ones:

Remember: someone (probably future-you) will need to understand and maintain this code. Make it easy for them!

Ready to make your package bulletproof with testing strategies? Let’s dive into the testing section next! 🧪

🧪 Testing Your Package

Here’s where we separate the professionals from the hobbyists. A package without tests is like a car without brakes — it might work now, but you’re heading for trouble.

The good news? Testing packages is actually easier than testing full iOS apps because packages have focused, isolated functionality. Let’s make your package bulletproof!

Why Package Testing Matters More

Trust Factor: When someone adds your package to their project, they’re trusting that your code won’t break their app. Tests prove that trust is warranted.

Confidence in Changes: Want to add a new feature or fix a bug? With good tests, you can make changes knowing you haven’t broken existing functionality.

Documentation: Well-written tests show exactly how your API is supposed to be used. They’re living examples of your package in action.

Unit Testing Your Package

Let’s start with unit tests — testing individual functions and methods in isolation:

import XCTest
@testable import MyUtilities

final class StringExtensionsTests: XCTestCase {
    
    // MARK: - Trimming Tests
    func testTrimming_withWhitespace_removesWhitespace() {
        // Given
        let input = "  hello world  "
        
        // When
        let result = input.trimmed
        
        // Then
        XCTAssertEqual(result, "hello world")
    }
    
    func testTrimming_withNewlines_removesNewlines() {
        // Given
        let input = "\n\nhello world\n\n"
        
        // When
        let result = input.trimmed
        
        // Then
        XCTAssertEqual(result, "hello world")
    }
    
    func testTrimming_withoutWhitespace_returnsUnchanged() {
        // Given
        let input = "hello world"
        
        // When
        let result = input.trimmed
        
        // Then
        XCTAssertEqual(result, "hello world")
    }
    
    func testTrimming_emptyString_returnsEmpty() {
        // Given
        let input = ""
        
        // When
        let result = input.trimmed
        
        // Then
        XCTAssertEqual(result, "")
    }
}

Notice the Pattern:

This Given-When-Then pattern makes tests incredibly readable and maintainable.

Testing Edge Cases

The difference between amateur and professional packages is edge case handling:

import XCTest
@testable import MyUtilities

final class EmailValidationTests: XCTestCase {
    
    func testValidEmails() {
        let validEmails = [
            "simple@example.com",
            "user.name@example.com",
            "user+tag@example.com",
            "x@example.co.uk",
            "test@subdomain.example.com"
        ]
        
        for email in validEmails {
            XCTAssertTrue(email.isValidEmail, "\(email) should be valid")
        }
    }
    
    func testInvalidEmails() {
        let invalidEmails = [
            "",                    // Empty string
            "invalid",             // No @ symbol
            "@example.com",        // No local part
            "user@",               // No domain
            "user@.com",           // Domain starts with dot
            "user name@example.com", // Space in local part
            "user@example",        // No TLD
            "user@@example.com"    // Double @
        ]
        
        for email in invalidEmails {
            XCTAssertFalse(email.isValidEmail, "\(email) should be invalid")
        }
    }
    
    func testEmailValidation_withUnicodeCharacters() {
        // Test international domain names
        XCTAssertFalse("user@例え.テスト".isValidEmail) // May be valid in real world, but our regex doesn't handle it
    }
}

Performance Testing

For utility functions, performance can matter:

import XCTest
@testable import MyUtilities

final class PerformanceTests: XCTestCase {
    
    func testEmailValidationPerformance() {
        let testEmail = "user@example.com"
        
        measure {
            for _ in 0..<1000 {
                _ = testEmail.isValidEmail
            }
        }
    }
    
    func testStringTrimmingPerformance() {
        let testString = "   hello world with lots of whitespace   \n\n\n"
        
        measure {
            for _ in 0..<10000 {
                _ = testString.trimmed
            }
        }
    }
}

Testing Package Dependencies

If your package depends on other packages, mock them to keep tests fast and reliable:

import XCTest
@testable import MyUtilities

// Mock for testing without real network calls
class MockNetworkSession {
    var mockData: Data?
    var mockError: Error?
    
    func dataTask(with url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        completion(mockData, nil, mockError)
    }
}
final class NetworkingTests: XCTestCase {
    var mockSession: MockNetworkSession!
    var networkManager: NetworkManager!
    
    override func setUp() {
        super.setUp()
        mockSession = MockNetworkSession()
        networkManager = NetworkManager(session: mockSession)
    }
    
    func testSuccessfulRequest() {
        // Given
        let expectedData = "test response".data(using: .utf8)
        mockSession.mockData = expectedData
        
        // When & Then
        let expectation = self.expectation(description: "Network request")
        networkManager.fetchData { result in
            switch result {
            case .success(let data):
                XCTAssertEqual(data, expectedData)
            case .failure:
                XCTFail("Expected success")
            }
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 1.0)
    }
}

Running Tests Like a Pro

Command Line Testing:

# Run all tests
swift test

# Run specific test file
swift test --filter StringExtensionsTests

# Run with coverage
swift test --enable-code-coverage

Xcode Testing:

Test Coverage Goals

Aim for these coverage targets:

Want to become a testing ninja? I’ve written comprehensive guides on both Unit Testing in Swift Made Easy and UI Testing Best Practices for iOS Apps that dive deep into advanced testing patterns, mocking strategies, and how to structure tests that actually help you catch bugs before your users do.

**Unit Testing in Swift Made Easy: A Beginner’s Guide With Real Examples** _A beginner’s guide to Swift unit testing — learn the basics, write your first tests, and improve your code quality._medium.comhttps://medium.com/swift-pal/unit-testing-in-swift-made-easy-a-beginners-guide-with-real-examples-0409f65e84f6

**UI Testing in SwiftUI (2025 Guide): Write End-to-End Tests for Reliable iOS Apps** _UI testing got you sweating bullets? 🫠 In this 2025 guide, you’ll learn how to write reliable end-to-end tests for…_medium.comhttps://medium.com/swift-pal/ui-testing-in-swiftui-2025-guide-write-end-to-end-tests-for-reliable-ios-apps-164e4458ffdf

Test Organization Tips

Group Related Tests:

final class StringExtensionsTests: XCTestCase {
    
    // MARK: - Trimming Tests
    func testTrimming_basicWhitespace() { /* ... */ }
    func testTrimming_newlines() { /* ... */ }
    
    // MARK: - Email Validation Tests  
    func testEmailValidation_validFormats() { /* ... */ }
    func testEmailValidation_invalidFormats() { /* ... */ }
    
    // MARK: - Edge Cases
    func testEmptyStrings() { /* ... */ }
    func testUnicodeHandling() { /* ... */ }
}

Use Descriptive Test Names:

The pattern is: test[FunctionName]_[Condition]_[ExpectedResult]

The Confidence Factor

Here’s what good tests give you:

Remember: tests aren’t just about catching bugs (though they do that). They’re about building confidence — in your code, in your package, and in yourself as a developer.

Ready to make your package official with proper versioning and publishing? Let’s tackle that next! 🏷️

🏷️ Versioning & Publishing

Alright, your package is tested and polished — time to make it official! Proper versioning is what separates a random collection of code from a professional library that others can depend on.

Understanding Semantic Versioning

Swift packages use Semantic Versioning (SemVer), which follows the pattern MAJOR.MINOR.PATCH:

1.2.3 Breakdown:

Real Examples:

Your First Release: Version 1.0.0

Let’s get your package ready for its debut:

Step 1: Final Pre-Release Checklist

Before tagging version 1.0.0, make sure you have:

Step 2: Commit Everything

git add .
git commit -m "Prepare for v1.0.0 - Initial release with String and Date extensions"
git push origin main

Step 3: Create Your First Tag

# Create and push the tag
git tag 1.0.0
git push origin 1.0.0

That’s it! Your package is now officially version 1.0.0. 🎉

Local vs Remote Packages

You have two options for how others (including you) can use your package:

Option 1: Keep It Local (Private Use)

Perfect for personal utilities or company-internal packages:

Option 2: Publish to GitHub (Public Use)

Ready to share with the world? Here’s how:

Create GitHub Repository:

  1. Go to GitHub.com and create a new repository
  2. Name it the same as your package (e.g., “MyUtilities”)
  3. Don’t initialize with README (you already have one)

Push Your Package:

git remote add origin https://github.com/yourusername/MyUtilities.git
git branch -M main
git push -u origin main
git push origin --tags  # Don't forget the tags!

Now Anyone Can Use It:

// In other projects' Package.swift
dependencies: [
    .package(url: "https://github.com/yourusername/MyUtilities.git", from: "1.0.0")
]

Managing Updates Like a Pro

As your package evolves, here’s how to handle updates:

Patch Updates (1.0.0 → 1.0.1)

For: Bug fixes, performance improvements, documentation updates

// Fixed a bug in email validation
public extension String {
    var isValidEmail: Bool {
        // Improved regex that handles more edge cases
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: self)
    }
}
git add .
git commit -m "Fix email validation regex for better edge case handling"
git tag 1.0.1
git push origin main --tags

Minor Updates (1.0.1 → 1.1.0)

For: New features that don’t break existing code

// Added new functionality
public extension String {
    // Existing functions unchanged...
    
    // New feature - doesn't break existing code
    var isValidPhoneNumber: Bool {
        let phoneRegEx = "^\\+?[1-9]\\d{1,14}$"
        let phonePred = NSPredicate(format:"SELF MATCHES %@", phoneRegEx)
        return phonePred.evaluate(with: self)
    }
}
git add .
git commit -m "Add phone number validation functionality"
git tag 1.1.0
git push origin main --tags

Major Updates (1.1.0 → 2.0.0)

For: Breaking changes that require users to update their code

// Breaking change - renamed function
public extension String {
    // OLD: var isValidEmail: Bool
    // NEW: More descriptive name
    var hasValidEmailFormat: Bool {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: self)
    }
}
git add .
git commit -m "BREAKING: Rename isValidEmail to hasValidEmailFormat for clarity"
git tag 2.0.0
git push origin main --tags

Pre-Release Versions

Testing major changes? Use pre-release versions:

git tag 2.0.0-beta.1
git tag 2.0.0-rc.1    # Release candidate

Users can opt into pre-releases:

.package(url: "https://github.com/yourusername/MyUtilities.git", from: "2.0.0-beta.1")

Dependency Management Best Practices

In Your Package.swift:

// Be specific about platform requirements
platforms: [
    .iOS(.v13),      // Don't go too low unless you need to
    .macOS(.v10_15), // Align with your actual testing
    .tvOS(.v13),
    .watchOS(.v6)
],

// Pin external dependencies to avoid surprises
dependencies: [
    .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"),
    // Not: .package(url: "...", .branch("main")) - too unstable
]

Release Notes & Changelog

Keep track of changes in a CHANGELOG.md:

# Changelog

## [1.1.0] - 2025-01-15

### Added
- Phone number validation functionality
- Better error messages for validation failures

### Changed
- Improved email regex for better international support

### Fixed
- Fixed trimming behavior with unicode whitespace

## [1.0.1] - 2025-01-10

### Fixed
- Email validation now handles more edge cases correctly

## [1.0.0] - 2025-01-05

### Added
- Initial release with String and Date extensions
- Email validation functionality
- String trimming utilities
- Relative date formatting

The Publishing Mindset

Once you publish version 1.0.0, you’re making a commitment:

But here’s the thing — don’t let this scare you! Every major iOS library started with someone’s first 1.0.0 release. The key is starting small and growing thoughtfully.

Your package is now officially ready for the world! 🌍

Wrap-Up

🏆 Your Package Transformation is Complete

Look at what you’ve accomplished! Your Swift package has evolved from a basic collection of utilities into a professional-grade library that:

You’re no longer just creating packages — you’re creating reliable software that other developers can depend on.

🎯 The Professional Difference

Here’s what separates your package now from typical “hobby projects”:

Before Part 2: “It works on my machine” After Part 2: “It works reliably for everyone, with proper documentation and testing”

That’s the difference between amateur and professional development. You’ve crossed that line! 🎓

🚀 Ready for Part 3: The Advanced Stuff

Your package is now professional-grade, but what about the real-world challenges you’ll face as your packages grow in complexity and popularity?

Part 3: “**Advanced Swift Package Management: Dependencies, Community & Career Growth** will cover:

Advanced Topics Preview:

Your Challenge Before Part 3 📝

Put your new skills to work:

  1. Apply these patterns to your existing package from Part 1
  2. Create a second package using the professional structure we covered
  3. Publish version 1.0.0 of at least one package
  4. Document any weird issues you encounter — we’ll tackle advanced troubleshooting in Part 3!

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading