Tüm Yazılar
KategoriDevOps
Okuma Süresi
28 dk
Yayın Tarihi
2024-02-05
Kelime Sayısı
2.327kelime

Kahveni hazırla - bu içerikli bir makale!

iOS CI/CD Pipeline: GitHub Actions ve Fastlane

Özet

GitHub Actions, Fastlane ve Xcode Cloud ile otomatik build, test ve deployment. Complete iOS DevOps guide.

  • GitHub Actions'ta 2000 dk/ay, Xcode Cloud'da 25 saat/ay ucretsiz kota sunuluyor.
  • Fastlane match ile appstore/development sertifikalari git repository uzerinden senkronize ediliyor.
  • -parallel-testing-enabled ve rastgele test sirasi flaky test tespitini hizlandiriyor.
  • Xcode Cloud'da ci_post_clone/ci_pre_xcodebuild/ci_post_xcodebuild script'leriyle pipeline ozellestiriliyor.
iOS CI/CD Pipeline: GitHub Actions ve Fastlane

Merhaba! Bugün iOS projelerinde profesyonel CI/CD pipeline kurulumunu A'dan Z'ye inceleyeceğiz. Bu rehber sonunda, kod push'ladığında otomatik build, test ve TestFlight deployment yapan bir sistem kurmuş olacaksın.


İçindekiler


🎯 Bu Yazıda Öğreneceklerin

  • GitHub Actions ile iOS CI/CD kurulumu
  • Fastlane ile build automation
  • Match ile code signing yönetimi
  • Xcode Cloud alternatifi
  • Test automation ve code coverage
  • Security best practices
  • Monitoring ve alerting

📚 CI/CD Nedir ve Neden Önemli?

CI (Continuous Integration): Her commit'te otomatik build ve test

CD (Continuous Delivery/Deployment): Otomatik TestFlight/App Store dağıtımı

iOS CI/CD Araçları Karşılaştırması

Araç
Maliyet
macOS Runner
Kurulum
GitHub Actions
2000 dk/ay ücretsiz
✅
Orta
Xcode Cloud
25 saat/ay ücretsiz
✅
Kolay
Bitrise
Sınırlı ücretsiz
✅
Kolay
CircleCI
Sınırlı ücretsiz
✅
Orta
Jenkins
Ücretsiz
Self-hosted
Zor
Fastlane
Ücretsiz (local)
Local
Kolay
💡 Altın İpucu: Başlangıç için Xcode Cloud, scale için GitHub Actions + Fastlane combo'su ideal!

Dış Kaynaklar:


🏗️ GitHub Actions Kurulumu

1. Temel Workflow Yapısı

yaml
1# .github/workflows/ios-ci.yml
2name: iOS CI/CD Pipeline
3 
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9 # Manual trigger
10 workflow_dispatch:
11 inputs:
12 deploy_target:
13 description: 'Deploy target'
14 required: true
15 default: 'testflight'
16 type: choice
17 options:
18 - testflight
19 - app-store
20 
21# Concurrent run'ları iptal et
22concurrency:
23 group: ${{ github.workflow }}-${{ github.ref }}
24 cancel-in-progress: true
25 
26env:
27 XCODE_VERSION: '15.2'
28 SCHEME: 'MyApp'
29 PROJECT: 'MyApp.xcodeproj'
30 # Veya xcworkspace için:
31 # WORKSPACE: 'MyApp.xcworkspace'
32 
33jobs:
34 # Job 1: Lint & Format Check
35 lint:
36 runs-on: macos-14
37 steps:
38 - uses: actions/checkout@v4
39
40 - name: Install SwiftLint
41 run: brew install swiftlint
42
43 - name: Run SwiftLint
44 run: swiftlint --strict --reporter github-actions-logging
45
46 - name: Check Swift Format
47 run: |
48 brew install swift-format
49 swift-format lint --recursive Sources/ Tests/
50
51 # Job 2: Build & Test
52 build-and-test:
53 needs: lint
54 runs-on: macos-14
55 timeout-minutes: 30
56
57 steps:
58 - name: Checkout
59 uses: actions/checkout@v4
60 with:
61 fetch-depth: 0 # Full history for versioning
62
63 - name: Setup Xcode
64 uses: maxim-lobanov/setup-xcode@v1
65 with:
66 xcode-version: ${{ env.XCODE_VERSION }}
67
68 # SPM Cache
69 - name: Cache SPM
70 uses: actions/cache@v4
71 with:
72 path: |
73 ~/Library/Developer/Xcode/DerivedData/**/SourcePackages
74 .build
75 key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
76 restore-keys: |
77 ${{ runner.os }}-spm-
78
79 # Ruby/Bundler for Fastlane
80 - name: Setup Ruby
81 uses: ruby/setup-ruby@v1
82 with:
83 ruby-version: '3.2'
84 bundler-cache: true
85
86 # Build
87 - name: Build for Testing
88 run: |
89 set -o pipefail
90 xcodebuild build-for-testing \
91 -scheme "${{ env.SCHEME }}" \
92 -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \
93 -configuration Debug \
94 -derivedDataPath DerivedData \
95 CODE_SIGNING_ALLOWED=NO \
96 | xcpretty --color
97
98 # Unit Tests
99 - name: Run Unit Tests
100 run: |
101 set -o pipefail
102 xcodebuild test-without-building \
103 -scheme "${{ env.SCHEME }}" \
104 -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \
105 -derivedDataPath DerivedData \
106 -resultBundlePath TestResults.xcresult \
107 -enableCodeCoverage YES \
108 | xcpretty --color --report junit --output test-results.xml
109
110 # UI Tests (opsiyonel)
111 - name: Run UI Tests
112 if: github.event_name == 'push' && github.ref == 'refs/heads/main'
113 run: |
114 xcodebuild test \
115 -scheme "${{ env.SCHEME }}UITests" \
116 -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \
117 -derivedDataPath DerivedData \
118 | xcpretty
119
120 # Upload test results
121 - name: Upload Test Results
122 uses: actions/upload-artifact@v4
123 if: always()
124 with:
125 name: test-results
126 path: |
127 TestResults.xcresult
128 test-results.xml
129 retention-days: 14
130
131 # Code Coverage
132 - name: Generate Coverage Report
133 run: |
134 xcrun xccov view --report TestResults.xcresult > coverage.txt
135 cat coverage.txt
136
137 - name: Upload Coverage to Codecov
138 uses: codecov/codecov-action@v4
139 with:
140 xcode: true
141 xcode_archive_path: TestResults.xcresult
142
143 # Job 3: Deploy to TestFlight
144 deploy-testflight:
145 needs: build-and-test
146 runs-on: macos-14
147 if: github.ref == 'refs/heads/main' && github.event_name == 'push'
148 environment: production
149
150 steps:
151 - uses: actions/checkout@v4
152
153 - uses: maxim-lobanov/setup-xcode@v1
154 with:
155 xcode-version: ${{ env.XCODE_VERSION }}
156
157 - uses: ruby/setup-ruby@v1
158 with:
159 ruby-version: '3.2'
160 bundler-cache: true
161
162 # App Store Connect API Key
163 - name: Setup ASC API Key
164 env:
165 ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
166 ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
167 ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
168 run: |
169 mkdir -p ~/.appstoreconnect/private_keys
170 echo "$ASC_KEY_CONTENT" > ~/.appstoreconnect/private_keys/AuthKey_$ASC_KEY_ID.p8
171
172 # Deploy via Fastlane
173 - name: Deploy to TestFlight
174 env:
175 MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
176 MATCH_GIT_PRIVATE_KEY: ${{ secrets.MATCH_GIT_PRIVATE_KEY }}
177 ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
178 ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
179 FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
180 run: |
181 bundle exec fastlane ios beta
182
183 # Slack/Discord notification
184 - name: Notify Success
185 if: success()
186 uses: slackapi/slack-github-action@v1
187 with:
188 payload: |
189 {
190 "text": "✅ New build deployed to TestFlight!",
191 "blocks": [
192 {
193 "type": "section",
194 "text": {
195 "type": "mrkdwn",
196 "text": "*MyApp* - Build #${{ github.run_number }} deployed to TestFlight"
197 }
198 }
199 ]
200 }
201 env:
202 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

🔧 Fastlane Kurulumu

2. Fastfile Konfigürasyonu

ruby
1# fastlane/Fastfile
2default_platform(:ios)
3 
4# ═══════════════════════════════════════════
5# CONSTANTS
6# ═══════════════════════════════════════════
7APP_IDENTIFIER = "com.mycompany.myapp"
8TEAM_ID = "XXXXXXXXXX"
9ITC_TEAM_ID = "XXXXXXXXXX"
10 
11# ═══════════════════════════════════════════
12# iOS Platform
13# ═══════════════════════════════════════════
14platform :ios do
15
16 # -----------------------------------------
17 # Before All
18 # -----------------------------------------
19 before_all do
20 # CI ortamında keychain setup
21 setup_ci if ENV['CI']
22
23 # Version info
24 UI.message("🚀 Running Fastlane on: #{lane_context[:PLATFORM_NAME]}")
25 end
26
27 # -----------------------------------------
28 # Error Handler
29 # -----------------------------------------
30 error do |lane, exception|
31 slack(
32 message: "❌ Fastlane failed: #{exception.message}",
33 success: false,
34 slack_url: ENV['SLACK_WEBHOOK']
35 ) if ENV['SLACK_WEBHOOK']
36 end
37
38 # ═══════════════════════════════════════════
39 # LANES
40 # ═══════════════════════════════════════════
41
42 # -----------------------------------------
43 # Test Lane
44 # -----------------------------------------
45 desc "Run all tests"
46 lane :test do
47 scan(
48 scheme: "MyApp",
49 device: "iPhone 15 Pro",
50 clean: true,
51 code_coverage: true,
52 output_types: "junit,html",
53 output_directory: "./fastlane/test_output",
54 result_bundle: true,
55 xcargs: "-skipPackagePluginValidation"
56 )
57 end
58
59 # -----------------------------------------
60 # Beta Lane (TestFlight)
61 # -----------------------------------------
62 desc "Build and deploy to TestFlight"
63 lane :beta do |options|
64 # Ensure clean state
65 ensure_git_status_clean unless options[:skip_git_check]
66
67 # Increment build number
68 increment_build_number(
69 build_number: ENV['GITHUB_RUN_NUMBER'] || latest_testflight_build_number + 1
70 )
71
72 # Code signing
73 match(
74 type: "appstore",
75 readonly: is_ci,
76 app_identifier: APP_IDENTIFIER
77 )
78
79 # Build
80 build_ios_app(
81 scheme: "MyApp",
82 configuration: "Release",
83 export_method: "app-store",
84 export_options: {
85 provisioningProfiles: {
86 APP_IDENTIFIER => "match AppStore #{APP_IDENTIFIER}"
87 }
88 },
89 xcargs: "-skipPackagePluginValidation",
90 include_bitcode: false,
91 output_directory: "./build",
92 output_name: "MyApp.ipa"
93 )
94
95 # Upload to TestFlight
96 upload_to_testflight(
97 api_key: app_store_connect_api_key,
98 skip_waiting_for_build_processing: true,
99 changelog: changelog_from_git_commits(
100 commits_count: 10,
101 merge_commit_filtering: "only_include_merges"
102 )
103 )
104
105 # Tag release
106 add_git_tag(
107 tag: "build/#{lane_context[SharedValues::BUILD_NUMBER]}"
108 )
109
110 # Notify
111 slack(
112 message: "🚀 New build uploaded to TestFlight!",
113 success: true,
114 slack_url: ENV['SLACK_WEBHOOK'],
115 default_payloads: [:git_branch, :git_author]
116 ) if ENV['SLACK_WEBHOOK']
117 end
118
119 # -----------------------------------------
120 # Release Lane (App Store)
121 # -----------------------------------------
122 desc "Deploy to App Store"
123 lane :release do |options|
124 # Version bump
125 version = options[:version] || prompt(text: "Enter version number:")
126 increment_version_number(version_number: version)
127 increment_build_number(build_number: 1)
128
129 # Build
130 match(type: "appstore", readonly: true)
131
132 build_ios_app(
133 scheme: "MyApp",
134 configuration: "Release",
135 export_method: "app-store"
136 )
137
138 # Upload with metadata
139 upload_to_app_store(
140 api_key: app_store_connect_api_key,
141 skip_screenshots: true,
142 skip_metadata: false,
143 precheck_include_in_app_purchases: false,
144 submit_for_review: false,
145 automatic_release: false,
146 force: true
147 )
148
149 # Commit and push
150 commit_version_bump(
151 message: "Release v#{version}",
152 xcodeproj: "MyApp.xcodeproj"
153 )
154
155 add_git_tag(tag: "v#{version}")
156 push_to_git_remote
157 end
158
159 # -----------------------------------------
160 # Certificates Lane
161 # -----------------------------------------
162 desc "Sync certificates"
163 lane :sync_certs do
164 match(type: "development", readonly: false)
165 match(type: "appstore", readonly: false)
166 end
167
168 # -----------------------------------------
169 # Screenshots Lane
170 # -----------------------------------------
171 desc "Capture screenshots"
172 lane :screenshots do
173 capture_screenshots(
174 workspace: "MyApp.xcworkspace",
175 scheme: "MyAppUITests",
176 devices: [
177 "iPhone 15 Pro Max",
178 "iPhone SE (3rd generation)",
179 "iPad Pro (12.9-inch) (6th generation)"
180 ],
181 languages: ["en-US", "tr"],
182 output_directory: "./fastlane/screenshots"
183 )
184
185 frame_screenshots(
186 path: "./fastlane/screenshots",
187 silver: true
188 )
189 end
190
191 # ═══════════════════════════════════════════
192 # PRIVATE LANES
193 # ═══════════════════════════════════════════
194
195 private_lane :app_store_connect_api_key do
196 app_store_connect_api_key(
197 key_id: ENV["ASC_KEY_ID"],
198 issuer_id: ENV["ASC_ISSUER_ID"],
199 key_filepath: "~/.appstoreconnect/private_keys/AuthKey_#{ENV['ASC_KEY_ID']}.p8",
200 in_house: false
201 )
202 end
203end

3. Matchfile (Code Signing)

ruby
1# fastlane/Matchfile
2git_url("git@github.com:mycompany/ios-certificates.git")
3storage_mode("git")
4 
5type("appstore")
6app_identifier(["com.mycompany.myapp"])
7username("developer@mycompany.com")
8 
9# Team settings
10team_id("XXXXXXXXXX")
11team_name("My Company")
12 
13# For CI
14readonly(is_ci)
15 
16# 🐣 Easter Egg: Gizli bypass
17# ENV['MATCH_SKIP_CONFIRM']='1' ile confirmation atla

🔐 Secrets Management

4. GitHub Secrets Kurulumu

yaml
1# Required secrets (Settings > Secrets > Actions):
2 
3# App Store Connect API
4ASC_KEY_ID: "XXXXXXXXXX"
5ASC_ISSUER_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
6ASC_KEY_CONTENT: |
7 -----BEGIN PRIVATE KEY-----
8 MIGTAgEAMB....
9 -----END PRIVATE KEY-----
10 
11# Match (Code Signing)
12MATCH_PASSWORD: "your-encryption-password"
13MATCH_GIT_PRIVATE_KEY: |
14 -----BEGIN OPENSSH PRIVATE KEY-----
15 xxxxx...
16 -----END OPENSSH PRIVATE KEY-----
17 
18# Notifications
19SLACK_WEBHOOK: "https://hooks.slack.com/services/..."

5. Keychain Yönetimi

ruby
1# fastlane/actions/setup_ci_keychain.rb
2# Custom action for CI keychain
3 
4module Fastlane
5 module Actions
6 class SetupCiKeychainAction < Action
7 def self.run(params)
8 keychain_name = "fastlane_ci_keychain"
9 keychain_password = SecureRandom.base64(32)
10
11 # Create keychain
12 create_keychain(
13 name: keychain_name,
14 password: keychain_password,
15 default_keychain: true,
16 unlock: true,
17 timeout: 3600,
18 lock_when_sleeps: false
19 )
20
21 # Return for cleanup
22 { name: keychain_name, password: keychain_password }
23 end
24 end
25 end
26end

🧪 Test Automation

6. Test Matrix Workflow

yaml
1# .github/workflows/test-matrix.yml
2name: Test Matrix
3 
4on:
5 pull_request:
6 branches: [main, develop]
7 
8jobs:
9 test-matrix:
10 strategy:
11 fail-fast: false
12 matrix:
13 os: [macos-13, macos-14]
14 xcode: ['15.0', '15.2']
15 destination:
16 - 'platform=iOS Simulator,name=iPhone 15,OS=17.2'
17 - 'platform=iOS Simulator,name=iPhone 14,OS=16.4'
18
19 runs-on: ${{ matrix.os }}
20 name: Xcode ${{ matrix.xcode }} - ${{ matrix.destination }}
21
22 steps:
23 - uses: actions/checkout@v4
24
25 - uses: maxim-lobanov/setup-xcode@v1
26 with:
27 xcode-version: ${{ matrix.xcode }}
28
29 - name: Run Tests
30 run: |
31 xcodebuild test \
32 -scheme "MyApp" \
33 -destination '${{ matrix.destination }}' \
34 -resultBundlePath "Results-${{ matrix.xcode }}.xcresult" \
35 CODE_SIGNING_ALLOWED=NO
36
37 - name: Upload Results
38 uses: actions/upload-artifact@v4
39 with:
40 name: test-results-${{ matrix.xcode }}
41 path: "*.xcresult"

📊 Code Quality & Metrics

7. Quality Gates

yaml
1# .github/workflows/quality.yml
2name: Code Quality
3 
4on: [pull_request]
5 
6jobs:
7 quality-gate:
8 runs-on: macos-14
9
10 steps:
11 - uses: actions/checkout@v4
12 with:
13 fetch-depth: 0
14
15 # SwiftLint
16 - name: SwiftLint
17 run: |
18 brew install swiftlint
19 swiftlint --strict --reporter json > swiftlint-report.json
20
21 # Complexity Analysis
22 - name: Analyze Complexity
23 run: |
24 brew install periphery
25 periphery scan --format json > periphery-report.json
26
27 # Dead Code Detection
28 - name: Dead Code Check
29 run: |
30 periphery scan --format xcode
31
32 # Dependency Check
33 - name: Check Outdated Dependencies
34 run: |
35 swift package show-dependencies --format json
36
37 # Size Analysis
38 - name: Binary Size Check
39 run: |
40 xcodebuild -scheme MyApp -configuration Release archive \
41 -archivePath MyApp.xcarchive \
42 CODE_SIGNING_ALLOWED=NO
43
44 # Check size
45 du -sh MyApp.xcarchive/Products/Applications/MyApp.app
46
47 # Report
48 - name: Generate Report
49 run: |
50 echo "## Code Quality Report" >> $GITHUB_STEP_SUMMARY
51 echo "- SwiftLint issues: $(cat swiftlint-report.json | jq length)" >> $GITHUB_STEP_SUMMARY

Okuyucu Ödülü

markdown
# 🎁 iOS CI/CD PRODUCTION CHECKLIST
 
## GitHub Actions Setup
- [ ] Workflow files oluşturuldu (.github/workflows/)
- [ ] macOS runner version belirlendi
- [ ] Xcode version sabitlendi
- [ ] SPM cache aktif
- [ ] Concurrent run cancellation aktif
 
## Fastlane Setup
- [ ] Fastfile oluşturuldu
- [ ] Matchfile konfigüre edildi
- [ ] Appfile oluşturuldu
- [ ] Gemfile dependencies tanımlandı
- [ ] .env.default dosyası oluşturuldu
 
## Code Signing
- [ ] Match repository oluşturuldu
- [ ] Development certificate sync
- [ ] App Store certificate sync
- [ ] Provisioning profiles sync
- [ ] CI keychain automation
 
## Secrets
- [ ] ASC_KEY_ID
- [ ] ASC_ISSUER_ID
- [ ] ASC_KEY_CONTENT
- [ ] MATCH_PASSWORD
- [ ] MATCH_GIT_PRIVATE_KEY
- [ ] SLACK_WEBHOOK (optional)
 
## Testing
- [ ] Unit test lane
- [ ] UI test lane
- [ ] Code coverage reporting
- [ ] Test matrix (multiple iOS versions)
- [ ] Snapshot testing
 
## Deployment
- [ ] TestFlight deployment
- [ ] App Store deployment
- [ ] Changelog generation
- [ ] Git tagging
- [ ] Release notes
 
## Monitoring
- [ ] Slack notifications
- [ ] Build time tracking
- [ ] Test coverage trends
- [ ] Error alerting
 
## Security
- [ ] Secrets rotation schedule
- [ ] Access audit
- [ ] Dependency vulnerability scan
- [ ] Code signing audit

<a id="cicd-xcode-cloud"></a>

⚡ Xcode Cloud Alternatifi

8. Xcode Cloud Kurulumu

swift
1// ci_scripts/ci_post_clone.sh
2#!/bin/sh
3 
4set -e
5 
6echo "🔧 Post-clone setup..."
7 
8# Install dependencies
9if [ -f "Brewfile" ]; then
10 brew bundle install
11fi
12 
13# Install Ruby gems
14if [ -f "Gemfile" ]; then
15 bundle install
16fi
17 
18# SwiftLint
19if command -v swiftlint &> /dev/null; then
20 swiftlint --strict
21fi
22 
23echo "✅ Post-clone complete"
24 
25// ci_scripts/ci_pre_xcodebuild.sh
26#!/bin/sh
27 
28set -e
29 
30echo "🏗️ Pre-build setup..."
31 
32# Increment build number
33if [ "$CI_XCODE_CLOUD" = "TRUE" ]; then
34 agvtool new-version -all $CI_BUILD_NUMBER
35fi
36 
37echo "✅ Pre-build complete"
38 
39// ci_scripts/ci_post_xcodebuild.sh
40#!/bin/sh
41 
42set -e
43 
44if [ "$CI_XCODEBUILD_EXIT_CODE" != "0" ]; then
45 echo "❌ Build failed with code: $CI_XCODEBUILD_EXIT_CODE"
46 exit 1
47fi
48 
49echo "✅ Build succeeded"
50 
51# Upload dSYMs to Crashlytics
52if [ "$CI_XCODEBUILD_ACTION" = "archive" ]; then
53 echo "📤 Uploading dSYMs..."
54 # Firebase Crashlytics upload script
55fi

💡 Pro Tips ve Best Practices

  1. Cache Everything - SPM, CocoaPods, DerivedData cache'le.
  1. Parallel Jobs - Lint, build, test'i paralel çalıştır.
  1. Fail Fast - İlk hata'da dur, zaman kaybet.
  1. macOS 14 Kullan - M1 runner'lar 3x daha hızlı.
  1. Self-hosted Runner - Yoğun projeler için kendi Mac mini'ni kur.
  1. Artifact Retention - Test sonuçlarını 14 gün tut, daha fazlası gereksiz.
  1. Environment Separation - Staging/production için ayrı workflow.
  1. Secrets Rotation - API key'leri 90 günde bir yenile.

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

📖 Sonuç

CI/CD, modern iOS development'ın olmazsa olmazı. GitHub Actions + Fastlane combo'su ile profesyonel düzeyde pipeline kurabilirsin.

Unutma: Başlangıçta 1-2 gün yatırım, sonrasında yüzlerce saat tasarruf!

Bir sonraki yazıda görüşmek üzere! 🚀


*CI/CD kurulumunda takıldın mı? Twitter'dan yaz!*

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Etiketler

#CI/CD#Fastlane#GitHub Actions#Xcode Cloud#DevOps#Automation
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik