Tüm Yazılar
KategoriDevOps
Okuma Süresi
28 dk
Yayın Tarihi
...
Kelime Sayısı
2.324kelime

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

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

iOS CI/CD Pipeline: GitHub Actions ve Fastlane

# iOS CI/CD Pipeline: Enterprise-Grade DevOps 🚀

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("[email protected]:mycompany/ios-certificates.git")
3storage_mode("git")
4 
5type("appstore")
6app_identifier(["com.mycompany.myapp"])
7username("[email protected]")
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

markdown
1# 🎁 iOS CI/CD PRODUCTION CHECKLIST
2 
3## GitHub Actions Setup
4- [ ] Workflow files oluşturuldu (.github/workflows/)
5- [ ] macOS runner version belirlendi
6- [ ] Xcode version sabitlendi
7- [ ] SPM cache aktif
8- [ ] Concurrent run cancellation aktif
9 
10## Fastlane Setup
11- [ ] Fastfile oluşturuldu
12- [ ] Matchfile konfigüre edildi
13- [ ] Appfile oluşturuldu
14- [ ] Gemfile dependencies tanımlandı
15- [ ] .env.default dosyası oluşturuldu
16 
17## Code Signing
18- [ ] Match repository oluşturuldu
19- [ ] Development certificate sync
20- [ ] App Store certificate sync
21- [ ] Provisioning profiles sync
22- [ ] CI keychain automation
23 
24## Secrets
25- [ ] ASC_KEY_ID
26- [ ] ASC_ISSUER_ID
27- [ ] ASC_KEY_CONTENT
28- [ ] MATCH_PASSWORD
29- [ ] MATCH_GIT_PRIVATE_KEY
30- [ ] SLACK_WEBHOOK (optional)
31 
32## Testing
33- [ ] Unit test lane
34- [ ] UI test lane
35- [ ] Code coverage reporting
36- [ ] Test matrix (multiple iOS versions)
37- [ ] Snapshot testing
38 
39## Deployment
40- [ ] TestFlight deployment
41- [ ] App Store deployment
42- [ ] Changelog generation
43- [ ] Git tagging
44- [ ] Release notes
45 
46## Monitoring
47- [ ] Slack notifications
48- [ ] Build time tracking
49- [ ] Test coverage trends
50- [ ] Error alerting
51 
52## Security
53- [ ] Secrets rotation schedule
54- [ ] Access audit
55- [ ] Dependency vulnerability scan
56- [ ] Code signing audit

Okuyucu Ödülü

⚡ 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.

yaml
1# Workflow debug için gizli SSH erişimi
2# workflow_dispatch input'una ekle:
3debug_enabled:
4 type: boolean
5 description: 'Enable SSH debug'
6 required: false
7 default: false
8 
9# Job'a ekle:
10- name: Debug SSH
11 if: ${{ inputs.debug_enabled }}
12 uses: mxschmitt/action-tmate@v3
13 with:
14 limit-access-to-actor: true

📖 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ı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

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

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz