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…

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:
- Your team wants to use it and encounters edge cases you never tested?
- You need to add features without breaking existing functionality?
- You want to share it publicly but realize the documentation is… well, nonexistent? 😅
**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:
- 📁 Professional Package Structure: Organizing code that scales beautifully
- 🧪 Testing Like a Pro: Comprehensive strategies that catch bugs before your users do
- 🏷️ Publishing & Versioning: Managing releases like the professional libraries you use daily
- 📚 Documentation That Actually Helps: Making your package approachable for others (and future-you!)
Where We’re Headed:
- Transform your “hobby project” into professional-grade code
- Learn testing patterns that prevent embarrassing production bugs
- Master semantic versioning and release management
- Create documentation that makes other developers love your package
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 interfaceWhy This Matters:
- Easy to find specific functionality
- Multiple people can work on different parts
- Cleaner git history when making changes
- Easier to test individual components
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(Xcode template works great)LICENSE(MIT is popular for open source)CHANGELOG.md(track your changes)
Gitignore Essentials:
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedataTesting 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:
- ✅ Consistent naming conventions throughout
- ✅ Comprehensive documentation with examples
- ✅ Good test coverage (aim for 80%+)
- ✅ Clear README with installation and usage
- ✅ Semantic versioning (we’ll cover this next)
- ✅ Organized file structure that scales
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:
- Given: Set up the test data
- When: Execute the function being tested
- Then: Assert the expected result
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-coverageXcode Testing:
Cmd+U: Run all tests- Click the diamond next to individual tests to run just that test
- Use the Test Navigator to see test results
Test Coverage Goals
Aim for these coverage targets:
- Public APIs: 100% (every public function should be tested)
- Critical paths: 100% (anything that could break user apps)
- Overall package: 80%+ (industry standard for quality code)
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:
- ❌
testTrimming() - ✅
testTrimming_withWhitespace_removesWhitespace()
The pattern is: test[FunctionName]_[Condition]_[ExpectedResult]
The Confidence Factor
Here’s what good tests give you:
- ✅ Confidence to refactor your code
- ✅ Documentation of how your API works
- ✅ Early warning when something breaks
- ✅ Professional credibility when others review your code
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:
- MAJOR (1): Breaking changes that require users to update their code
- MINOR (2): New features that don’t break existing functionality
- PATCH (3): Bug fixes and small improvements
Real Examples:
1.0.0→1.0.1: Fixed a bug in email validation1.0.1→1.1.0: Added new String extensions1.1.0→2.0.0: Changed public API signatures (breaking change)
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:
- ✅ All tests passing (
swift test) - ✅ Documentation complete (README, code comments)
- ✅ Public API finalized (you’re committing to this interface)
- ✅ No TODO comments in public code
- ✅ License file added (MIT is popular for open source)
Step 2: Commit Everything
git add .
git commit -m "Prepare for v1.0.0 - Initial release with String and Date extensions"
git push origin mainStep 3: Create Your First Tag
# Create and push the tag
git tag 1.0.0
git push origin 1.0.0That’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:
- Store in a shared network drive or private Git server
- Add to projects using “Add Local Package”
- Great for proprietary code or work-in-progress packages
Option 2: Publish to GitHub (Public Use)
Ready to share with the world? Here’s how:
Create GitHub Repository:
- Go to GitHub.com and create a new repository
- Name it the same as your package (e.g., “MyUtilities”)
- 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 --tagsMinor 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 --tagsMajor 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 --tagsPre-Release Versions
Testing major changes? Use pre-release versions:
git tag 2.0.0-beta.1
git tag 2.0.0-rc.1 # Release candidateUsers 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 formattingThe Publishing Mindset
Once you publish version 1.0.0, you’re making a commitment:
- Stability: Your public API won’t change without a major version bump
- Support: You’ll fix critical bugs in reasonable time
- Communication: You’ll document changes and breaking updates
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:
- ✅ Scales beautifully with organized, modular structure
- ✅ Prevents bugs with comprehensive test coverage
- ✅ Manages updates professionally with semantic versioning
- ✅ Welcomes users with clear documentation and examples
- ✅ Builds trust through proper release management
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:
- 🔗 Dependency Hell Solutions: Managing complex dependency trees without conflicts
- ⚠️ Common Gotchas: Real-world problems that break packages (and how to avoid them)
- 🌟 Building Community: Turning your packages into open source success stories
- 💼 Career Impact: How package creation can transform your professional opportunities
Advanced Topics Preview:
- Handling the “diamond dependency problem”
- Debugging mysterious package integration failures
- Managing breaking changes gracefully
- Building a developer brand through quality packages
- Turning package expertise into consulting opportunities
Your Challenge Before Part 3 📝
Put your new skills to work:
- Apply these patterns to your existing package from Part 1
- Create a second package using the professional structure we covered
- Publish version 1.0.0 of at least one package
- 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! 🚀
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
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read