Pablo Ruiz | BlogPablo Ruiz's personal blog about software development, technology, and programming.https://pruizlezcano.github.io/en-usBuilding GitHub Contribution Graph in SwiftUIhttps://pruizlezcano.github.io/blog/building-github-contribution-graph-swiftui/https://pruizlezcano.github.io/blog/building-github-contribution-graph-swiftui/Learn how to build a GitHub-style contribution graph in SwiftUIMon, 25 Nov 2024 08:00:00 GMT<p>GitHub's contribution graph has become an iconic way to visualize activity over time. In this tutorial, we'll build a <code>ContributionChartView</code> component in SwiftUI that mimics the familiar green squares in GitHub's contribution graph. This component can be used to effectively visualize a wide range of activity data, from personal goals to team productivity.</p> <h2>What We'll Build</h2> <p>Our <code>ContributionChartView</code> component takes an array of <code>Contribution</code> values, organizes them into a grid layout, and uses a color gradient to represent the "contribution" intensity of each cell.</p> <p>Before diving into the code, let's outline our key goals for this project:</p> <p><img src="../../assets/github-contribution-graph-swiiftui.webp" alt="Github contribution graph preview" /></p> <h3>Key Features:</h3> <ul> <li><strong>Customizable Time Span</strong>: Choose how many weeks to display.</li> <li><strong>Flexible Start of Week</strong>: Set the start day of the week (e.g., Sunday or Monday).</li> <li><strong>Adjustable Cell Attributes</strong>: Customize color, size, spacing, and corner radius.</li> <li><strong>Optional Legend</strong>: Toggle an intensity scale to enhance understanding.</li> </ul> <p>Now that we have a clear understanding of what we're building, let's explore the technical implementation.</p> <h2>Structuring the <code>ContributionChartView</code></h2> <p>We'll start by defining the structure of our <code>ContributionChartView</code> and the supporting data types:</p> <pre><code>struct ContributionChartView: View { let contributions: [Contribution] let weeks: Int let firstDayOfWeek: WeekDay let targetValue: Double let cellColor: Color let emptyCellColor: Color let cellSize: CGFloat let cellSpacing: CGFloat let cellCornerRadius: CGFloat let showLegend: Bool private let calendar: Calendar } struct Contribution { let date: Date let count: Double } enum WeekDay: Int { case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday } </code></pre> <p>The <code>ContributionChartView</code> struct takes several parameters that define the input data, layout, and visual appearance of the contribution chart:</p> <ul> <li><code>contributions</code>: An array of <code>Contribution</code> structs, each representing a contribution on a specific date with a count value.</li> <li><code>weeks</code>: The number of weeks to display in the chart.</li> <li><code>firstDayOfWeek</code>: The day of the week that the chart should start with (e.g., Sunday or Monday).</li> <li><code>targetValue</code>: The maximum value in the dataset, used to scale the color intensity of each cell.</li> <li><code>cellColor</code>: The color used for cells with contributions.</li> <li><code>emptyCellColor</code>: The color used for cells without contributions.</li> <li><code>cellSize</code>, <code>cellSpacing</code>, <code>cellCornerRadius</code>: The size, spacing, and corner radius of each cell in the grid.</li> <li><code>showLegend</code>: A boolean flag to determine whether the legend should be displayed.</li> </ul> <h3>Initializing the Calendar</h3> <p>To ensure the correct grid layout based on the user's preferred start of week, we set up a <code>Calendar</code> instance with a customizable <code>firstDayOfWeek</code>:</p> <pre><code>struct ContributionChartView: View { // ... init( firstDayOfWeek: WeekDay = .sunday, // other parameters ) { // ... var calendar = Calendar.current calendar.firstWeekday = firstDayOfWeek.rawValue self.calendar = calendar } } </code></pre> <p>In this code, we create a new <code>Calendar</code> instance and set its <code>firstWeekday</code> property to the <code>rawValue</code> of the <code>firstDayOfWeek</code> parameter. This ensures that the grid layout in the contribution chart will start on the user's preferred day of the week.</p> <h2>Defining the Grid Data Structure</h2> <p>Next, we'll define a helper struct, <code>ContributionCell</code>, to represent each cell in the grid. Each cell will hold its position, date, and contribution intensity based on the corresponding data value.</p> <pre><code>struct ContributionCell: Identifiable { let id = UUID() let row: Int let column: Int let value: Int let date: Date let intensity: Double init(date: Date, value: Int, targetValue: Double, startOfWeek: Date, firstDayOfWeek: WeekDay) { let calendar = Calendar.current self.date = date self.value = value let weekday = calendar.component(.weekday, from: date) let firstDayOffset = firstDayOfWeek.rawValue row = (weekday - firstDayOffset + 7) % 7 let weeks = calendar.dateComponents([.weekOfYear], from: startOfWeek, to: date).weekOfYear ?? 0 column = weeks intensity = min(Double(value) / targetValue, 1.0) } } </code></pre> <p>We then create the <code>chartData</code> array, which is an array of <code>ContributionCell</code> instances representing the contributions over the specified time range:</p> <pre><code>struct ContributionChartView: View { // ... private var chartData: [ContributionCell] { let today = Date() var dateComponents = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: today) dateComponents.weekOfYear = (dateComponents.weekOfYear ?? 0) - weeks + 1 let startDate = calendar.date(from: dateComponents)! let contributionsDict = Dictionary( grouping: contributions, by: { calendar.startOfDay(for: $0.date) } ).mapValues { $0.reduce(0) { $0 + $1.count } } var cells: [ContributionCell] = [] var currentDate = startDate while currentDate &lt;= today { let startOfDay = calendar.startOfDay(for: currentDate) cells.append(ContributionCell( date: startOfDay, value: contributionsDict[startOfDay] ?? 0, targetValue: targetValue, startOfWeek: startDate, firstDayOfWeek: firstDayOfWeek )) currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)! } return cells } } </code></pre> <p>In this code, we first calculate the <code>startDate</code> for the chart based on the <code>weeks</code> parameter and the current date. We then group the <code>contributions</code> array by the start of each day, summing up the contribution counts for each day. Next, we loop through the days from the <code>startDate</code> to the current date, creating a <code>ContributionCell</code> for each day and adding it to the <code>cells</code> array. The <code>ContributionCell</code> struct is responsible for calculating the row, column, and intensity value for each cell based on the contribution data and the <code>firstDayOfWeek</code> setting. This <code>chartData</code> array serves as the data source for rendering the contribution chart grid.</p> <h2>Building the View's Body</h2> <p>The <code>body</code> of our <code>ContributionChartView</code> contains a vertically stacked grid for the contributions, along with an optional legend at the bottom:</p> <pre><code>struct ContributionChartView: View { // ... var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .top, spacing: cellSpacing) { ForEach(0 ..&lt; weeks, id: \.self) { week in VStack(spacing: cellSpacing) { ForEach(0 ..&lt; 7) { row in if let cell = chartData.first(where: { $0.column == week &amp;&amp; $0.row == row }) { Rectangle() .fill(colorForIntensity(cell.intensity)) .frame(width: cellSize, height: cellSize) .clipShape(RoundedRectangle(cornerRadius: cellCornerRadius)) .overlay( Rectangle() .stroke(Color.gray.opacity(0.1), lineWidth: 1) ) } } } } } if showLegend { HStack(spacing: 4) { Text("Less") .font(.caption) ForEach(0 ..&lt; 5) { i in Rectangle() .fill(colorForIntensity(Double(i) / 4.0)) .frame(width: cellSize, height: cellSize) .clipShape(RoundedRectangle(cornerRadius: cellCornerRadius)) } Text("More") .font(.caption) } } } .padding() } private func colorForIntensity(_ intensity: Double) -&gt; Color { intensity == 0 ? emptyCellColor : cellColor.opacity(0.2 + intensity * 0.8) } } </code></pre> <p>In this code, we first create a grid of contribution cells using nested <code>HStack</code> and <code>VStack</code> views. Each cell is represented by a <code>Rectangle</code> view, with its color intensity determined by the <code>colorForIntensity</code> function.</p> <p>The <code>colorForIntensity</code> function takes a value between 0 and 1, representing the contribution intensity, and returns a color in the specified gradient. Cells with no contributions are rendered using the <code>emptyCellColor</code>.</p> <p>If the <code>showLegend</code> flag is set to <code>true</code>, we also display a legend at the bottom of the chart. The legend is a horizontal stack of rectangles, each representing a different contribution intensity level, with the "Less" and "More" labels on either side.</p> <h2>Adding Platform Specific Colors</h2> <p>To ensure a consistent look and feel across macOS, iOS, and watchOS, we define some custom colors with platform-specific defaults:</p> <pre><code>extension Color { #if os(macOS) static let background = Color(NSColor.windowBackgroundColor) static let secondaryBackground = Color(NSColor.underPageBackgroundColor) static let tertiaryBackground = Color(NSColor.controlBackgroundColor) #endif #if os(iOS) static let background = Color(UIColor.systemBackground) static let secondaryBackground = Color(UIColor.secondarySystemBackground) static let tertiaryBackground = Color(UIColor.tertiarySystemBackground) #endif #if os(watchOS) static let background = Color.black #endif } </code></pre> <p>This extension allows us to use platform-specific colors for the background, secondary background, and tertiary background, ensuring that the <code>ContributionChartView</code> looks and feels at home on any Apple platform.</p> <h2>Usage Example</h2> <p>Here's an example of how to use the <code>ContributionChartView</code>:</p> <pre><code>struct ContentView: View { let calendar = Calendar.current let today = Date() let weeks = 16 let sampleData: [Contribution] = (0..&lt;7*weeks).map { dayOffset in let date = calendar.date(byAdding: .day, value: -dayOffset, to: today)! return Contribution(date: date, count: Int.random(in: 1...5)) } ContributionChartView( contributions: sampleData, weeks: weeks, firstDayOfWeek: .sunday, targetValue: 4, cellColor: .green, emptyCellColor: .secondaryBackground ) } </code></pre> <h2>Conclusion</h2> <p>The <code>ContributionChartView</code> provides a powerful, customizable SwiftUI component for visualizing contributions, goals, or any data suitable for a grid format. This project demonstrates SwiftUI's flexibility with data-driven layouts and gradient styling, opening the door to more complex data visualizations.</p> <p>You can find the full source code on <a href="https://github.com/pruizlezcano/contribution-graph-swiftui">GitHub</a>.</p> Distributing Your Mac App Beyond the App Storehttps://pruizlezcano.github.io/blog/distributing-mac-app/https://pruizlezcano.github.io/blog/distributing-mac-app/Learn how to distribute your Mac app beyond the App Store with essential steps like code signing, notarization, and alternative methods.Wed, 29 Oct 2025 08:47:29 GMT<p>When it comes to distributing your Mac app, the Mac App Store is often the first place developers think of. However, there are many reasons why you might want to distribute your app outside of the App Store, such as avoiding Apple's review process, reaching a broader audience, or maintaining more control over your app's distribution.</p> <p>In this post, we'll explore the steps you need to take to distribute your Mac app beyond the App Store, including code signing, notarization, and alternative distribution methods.</p> <blockquote> <p>[!NOTE] This is the process I follow, but it may not be the only way or the best way. Always refer to the latest <a href="https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution">Apple documentation</a> for the most accurate and up-to-date information.</p> </blockquote> <h2>TL;DR</h2> <ol> <li>Code Sign your app with a Developer ID certificate.</li> <li>Create a DMG (disk image) for your app.</li> <li>Notarize both the <code>.app</code> and the <code>.dmg</code> files.</li> <li>Staple the notarization tickets to both files.</li> <li>Zip the <code>.app</code> file for distribution.</li> <li>Distribute your app via your website, email, or third-party platforms.</li> </ol> <h2>Code Signing</h2> <p>To distribute your Mac app outside the App Store, you need to sign it with a Developer ID certificate. This ensures that your app is trusted by macOS and can be installed by users without security warnings.</p> <p>You can obtain a Developer ID certificate from the Apple Developer website. Once you have the certificate, you can sign your app using the <code>codesign</code> command in Terminal:</p> <pre><code>codesign --sign "Developer ID Application: Your Name (Team ID)" \ --timestamp \ --options runtime \ /path/to/YourApp.app </code></pre> <p>If you are using XCode, you can set the signing options in your project settings to automatically sign your app during the build process and export it as a Developer ID signed app.</p> <h2>Create a DMG</h2> <p>After signing your app, create a DMG (disk image) file for distribution. A DMG provides a professional way to distribute your app and allows users to easily drag and drop your app into their Applications folder.</p> <p>The easiest way is to use <a href="https://github.com/sindresorhus/create-dmg"><strong>create-dmg</strong></a>, which creates a beautiful DMG with proper settings:</p> <pre><code># Install create-dmg npm install --global create-dmg # Create the DMG create-dmg YourApp.app </code></pre> <h2>Notarize</h2> <p>Notarization is an additional security measure that Apple requires for apps distributed outside the App Store. It involves submitting your app to Apple for automated scanning to check for malicious content.</p> <p>Before submitting, you need to compress your app into a ZIP file. You can do this using the following command:</p> <pre><code>cd /path/to/your/app ditto -c -k YourApp.app YourApp.zip </code></pre> <p>Once your app is zipped, you can submit it for notarization using the <code>xcrun notarytool</code> command:</p> <pre><code>xcrun notarytool submit -p notarytool &lt;file&gt; </code></pre> <p>You can check the progress of your notarization request with:</p> <pre><code>xcrun notarytool history -p notarytool </code></pre> <p>Repeat the notarization process for the DMG file as well.</p> <h2>Staple</h2> <p>After your app has been notarized, you need to staple the notarization ticket to your app. This ensures that users can run your app even if they are offline. You can staple the ticket using the following command:</p> <pre><code>xcrun stapler staple YourApp.app </code></pre> <p>Repeat this step for the DMG file as well.</p> <h2>Conclusion</h2> <p>Distributing your Mac app outside the App Store gives you more control and flexibility, but it comes with additional responsibilities. Following Apple's code signing and notarization requirements ensures that your users can install and run your app safely without security warnings.</p> <p>The key takeaways are:</p> <ul> <li><strong>Always sign your code</strong> with a valid Developer ID Application certificate</li> <li><strong>Use the DMG format</strong> for professional, secure distribution</li> <li><strong>Sign every component</strong> in the correct order: app first, then DMG</li> <li><strong>Only notarize the outermost container</strong> (the DMG in this case)</li> <li><strong>Staple the ticket</strong> to ensure offline installation works</li> <li><strong>Test thoroughly</strong> on different Macs and scenarios before releasing</li> </ul> <p>While the process may seem complex at first, it becomes straightforward once you understand the workflow. Consider automating these steps with a build script or CI/CD pipeline for consistent, repeatable releases.</p> <p>Remember that Apple's policies and tools evolve over time, so always check the <a href="https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution">official documentation</a> for the latest requirements and best practices.</p>