- Published on
モバイルネイティブ開発完全ガイド2025: Swift, Kotlin, SwiftUI vs Jetpack Compose
- Authors

- Name
- Youngju Kim
- @fjvbn20031
1. なぜネイティブ開発(かいはつ)なのか?
2026年現在、React Native、Flutter、KMP(Kotlin Multiplatform)などのクロスプラットフォームソリューションが成熟しているにもかかわらず、ネイティブ開発の重要性(じゅうようせい)は依然(いぜん)として絶対的(ぜったいてき)である。高性能(こうせいのう)ゲーム、カメラ/ARアプリ、システムAPIの深い活用、最新OS機能の即時(そくじ)採用が必要なシナリオではネイティブが優位(ゆうい)である。
ネイティブ vs クロスプラットフォーム判断(はんだん)マトリクス
| 基準(きじゅん) | ネイティブ推奨(すいしょう) | クロスプラットフォーム推奨 |
|---|---|---|
| 性能(せいのう)要件 | 60fps+ゲーム、AR/VR | 一般(いっぱん)業務(ぎょうむ)アプリ |
| プラットフォームAPI | HealthKit、ARKit、CarPlay | 基本(きほん)カメラ、位置(いち) |
| チーム規模(きぼ) | 大規模(iOS/Android分離) | 小規模(単一(たんいつ)コードベース) |
| リリース速度 | 段階的(だんかいてき)、品質(ひんしつ)優先 | 高速(こうそく)MVP |
| OS新機能採用 | 重要(じゅうよう) | 後回(あとまわ)し |
2026年のネイティブ開発環境
- iOS 18、Swift 6.1、Xcode 16
- Android 15、Kotlin 2.0、Android Studio Ladybug
- SwiftUI 6 + UIKit interop強化
- Jetpack Compose 1.7 + Material 3 Expressive
2. Swift 6: コンパイル時安全性(あんぜんせい)の新時代
Swift 6は2024年9月にリリースされたメジャー版で、strict concurrencyがデフォルトとなり、データ競合(きょうごう)をコンパイル時に検出(けんしゅつ)する。
2.1 Strict Concurrency
class Counter {
var value = 0 // Sendableではない
func increment() {
value += 1
}
}
Task {
let counter = Counter()
Task { counter.increment() } // エラー
Task { counter.increment() }
}
// 解決(かいけつ)1: actor
actor Counter {
var value = 0
func increment() {
value += 1
}
}
// 解決2: @MainActor
@MainActor
class Counter {
var value = 0
func increment() {
value += 1
}
}
2.2 Typed Throws
enum NetworkError: Error {
case timeout
case noConnection
case serverError(Int)
}
func loadData() throws(NetworkError) -> Data {
throw .timeout
}
do {
let data = try loadData()
} catch .timeout {
print("Timeout")
} catch .serverError(let code) {
print("Server error: \(code)")
}
2.3 Embedded Swift
リソース制約(せいやく)の厳(きび)しい環境(マイクロコントローラ、組(く)み込(こ)み)でSwiftが利用可能。メタデータ/リフレクション削除(さくじょ)、ARCのみ。
@_silgen_name("led_on")
func ledOn()
@main
struct MyApp {
static func main() {
while true {
ledOn()
}
}
}
3. Kotlin 2.0: K2コンパイラの性能革命(かくめい)
Kotlin 2.0はK2コンパイラをデフォルト採用し、コンパイル速度が最大2倍(ばい)に向上(こうじょう)した。
3.1 K2コンパイラの主要(しゅよう)変更点
fun process(input: Any?) {
if (input is String && input.isNotEmpty()) {
listOf(1, 2, 3).forEach {
println(input.length)
}
}
}
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val updated = user.copy(age = 31)
3.2 Kotlin Multiplatform (KMP) Stable
// commonMain
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): User = api.fetchUser(id)
}
// iosMain
actual class Platform {
actual val name: String = "iOS ${UIDevice.currentDevice.systemVersion}"
}
// androidMain
actual class Platform {
actual val name: String = "Android ${Build.VERSION.SDK_INT}"
}
3.3 Coroutines 1.9 + Flow
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = userRepository.getUser(id)
_state.value = UiState.Success(user)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
class SearchViewModel : ViewModel() {
private val query = MutableStateFlow("")
val results: StateFlow<List<Item>> = query
.debounce(300)
.distinctUntilChanged()
.filter { it.length >= 2 }
.flatMapLatest { searchRepository.search(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
4. SwiftUI Deep Dive
4.1 Viewプロトコル
import SwiftUI
struct ContentView: View {
@State private var counter = 0
@State private var name = ""
var body: some View {
VStack(spacing: 20) {
Text("Hello, \(name.isEmpty ? "World" : name)!")
.font(.largeTitle)
.foregroundStyle(.primary)
TextField("Enter name", text: $name)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button("Increment: \(counter)") {
counter += 1
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
4.2 状態(じょうたい)管理(かんり)
struct LocalStateView: View {
@State private var isOn = false
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
struct ChildView: View {
@Binding var value: String
var body: some View {
TextField("Input", text: $value)
}
}
@Observable
class UserStore {
var users: [User] = []
var isLoading = false
func fetchUsers() async {
isLoading = true
defer { isLoading = false }
users = await api.getUsers()
}
}
struct UserListView: View {
@State private var store = UserStore()
var body: some View {
List(store.users) { user in
Text(user.name)
}
.task {
await store.fetchUsers()
}
}
}
4.3 アニメーション
struct AnimatedView: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.blue.gradient)
.frame(
width: isExpanded ? 300 : 100,
height: isExpanded ? 200 : 100
)
.animation(.spring(response: 0.5, dampingFraction: 0.7), value: isExpanded)
.onTapGesture {
isExpanded.toggle()
}
Image(systemName: "heart.fill")
.symbolEffect(.bounce, value: isExpanded)
.foregroundStyle(.red)
.font(.system(size: 60))
}
}
}
4.4 NavigationStack
struct AppNavigation: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
NavigationLink("Profile", value: Route.profile)
NavigationLink("Settings", value: Route.settings)
}
.navigationDestination(for: Route.self) { route in
switch route {
case .profile:
ProfileView()
case .settings:
SettingsView()
case .detail(let id):
DetailView(id: id)
}
}
}
}
}
enum Route: Hashable {
case profile
case settings
case detail(id: String)
}
5. Jetpack Compose Deep Dive
5.1 Composable関数(かんすう)
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello, $name!",
modifier = modifier.padding(16.dp),
style = MaterialTheme.typography.headlineMedium
)
}
@Composable
fun ContentScreen() {
var counter by remember { mutableStateOf(0) }
var name by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Hello, ${name.ifEmpty { "World" }}!",
style = MaterialTheme.typography.headlineLarge
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Enter name") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = { counter++ },
modifier = Modifier.fillMaxWidth()
) {
Text("Increment: $counter")
}
}
}
5.2 State Hoisting
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(
count = count,
onIncrement = { count++ }
)
}
@Composable
fun StatelessCounter(
count: Int,
onIncrement: () -> Unit
) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}
5.3 Side Effects
@Composable
fun UserScreen(userId: String, viewModel: UserViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
DisposableEffect(Unit) {
val listener = LocationListener()
locationManager.addListener(listener)
onDispose {
locationManager.removeListener(listener)
}
}
when (val s = state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> UserDetail(s.user)
is UiState.Error -> ErrorView(s.message)
}
}
5.4 Navigation Compose
@Composable
fun AppNavHost() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToProfile = { navController.navigate("profile") },
onNavigateToDetail = { id -> navController.navigate("detail/$id") }
)
}
composable("profile") {
ProfileScreen()
}
composable(
route = "detail/{id}",
arguments = listOf(navArgument("id") { type = NavType.StringType })
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id") ?: ""
DetailScreen(id = id)
}
}
}
6. アーキテクチャパターン
6.1 MVVM
@Observable
class ProductListViewModel {
var products: [Product] = []
var isLoading = false
var error: String?
private let repository: ProductRepository
init(repository: ProductRepository) {
self.repository = repository
}
func loadProducts() async {
isLoading = true
error = nil
do {
products = try await repository.fetchProducts()
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
}
struct ProductListView: View {
@State private var viewModel: ProductListViewModel
init(repository: ProductRepository) {
_viewModel = State(initialValue: ProductListViewModel(repository: repository))
}
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
Text("Error: \(error)")
} else {
List(viewModel.products) { product in
ProductRow(product: product)
}
}
}
.task {
await viewModel.loadProducts()
}
}
}
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
init {
loadProducts()
}
fun loadProducts() {
viewModelScope.launch {
_state.value = UiState.Loading
try {
val products = repository.fetchProducts()
_state.value = UiState.Success(products)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
6.2 TCA (The Composable Architecture)
import ComposableArchitecture
@Reducer
struct CounterFeature {
@ObservableState
struct State: Equatable {
var count = 0
var fact: String?
var isLoading = false
}
enum Action {
case incrementButtonTapped
case decrementButtonTapped
case factButtonTapped
case factResponse(String)
}
@Dependency(\.numberFactClient) var numberFact
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .incrementButtonTapped:
state.count += 1
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .factButtonTapped:
state.isLoading = true
return .run { [count = state.count] send in
let fact = try await numberFact.fetch(count)
await send(.factResponse(fact))
}
case let .factResponse(fact):
state.fact = fact
state.isLoading = false
return .none
}
}
}
}
6.3 Clean Architecture
Presentation Layer (UI)
↓
Domain Layer (UseCases, Entities)
↓
Data Layer (Repositories, DataSources)
7. 非同期処理(ひどうきしょり)
7.1 Swift Concurrency
func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
func fetchUserData(id: String) async throws -> UserData {
async let profile = fetchProfile(id: id)
async let posts = fetchPosts(userId: id)
async let followers = fetchFollowers(userId: id)
return try await UserData(
profile: profile,
posts: posts,
followers: followers
)
}
func fetchAllUsers(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
actor BankAccount {
private var balance: Decimal = 0
func deposit(_ amount: Decimal) {
balance += amount
}
func withdraw(_ amount: Decimal) throws -> Decimal {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
return amount
}
func getBalance() -> Decimal {
balance
}
}
7.2 Kotlin Coroutines
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
val response = httpClient.get("https://api.example.com/users/$id")
Json.decodeFromString<User>(response.body())
}
suspend fun fetchUserData(id: String): UserData = coroutineScope {
val profile = async { fetchProfile(id) }
val posts = async { fetchPosts(id) }
val followers = async { fetchFollowers(id) }
UserData(
profile = profile.await(),
posts = posts.await(),
followers = followers.await()
)
}
class SearchViewModel : ViewModel() {
private val queryFlow = MutableStateFlow("")
val searchResults: Flow<List<Result>> = queryFlow
.debounce(300)
.filter { it.length >= 2 }
.distinctUntilChanged()
.mapLatest { query ->
searchApi.search(query)
}
.catch { e ->
emit(emptyList())
}
}
8. 依存性(いぞんせい)注入(ちゅうにゅう)
8.1 iOS - Swinject
import Swinject
let container = Container()
container.register(NetworkService.self) { _ in
NetworkServiceImpl(baseURL: "https://api.example.com")
}
container.register(UserRepository.self) { resolver in
UserRepositoryImpl(network: resolver.resolve(NetworkService.self)!)
}
container.register(UserViewModel.self) { resolver in
UserViewModel(repository: resolver.resolve(UserRepository.self)!)
}
let viewModel = container.resolve(UserViewModel.self)!
8.2 Android - Hilt
@HiltAndroidApp
class MyApplication : Application()
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()
9. テスト
9.1 iOS - Swift Testing
import Testing
@testable import MyApp
@Suite("UserViewModel Tests")
struct UserViewModelTests {
@Test("Loading user updates state correctly")
func loadUser() async throws {
let mockRepo = MockUserRepository()
mockRepo.userToReturn = User(id: "1", name: "Alice")
let viewModel = UserViewModel(repository: mockRepo)
await viewModel.loadUser(id: "1")
#expect(viewModel.user?.name == "Alice")
#expect(viewModel.isLoading == false)
}
}
9.2 Android - JUnit + Compose Testing
@RunWith(AndroidJUnit4::class)
class UserViewModelTest {
@get:Rule
val coroutineRule = MainCoroutineRule()
private lateinit var viewModel: UserViewModel
private lateinit var repository: FakeUserRepository
@Before
fun setup() {
repository = FakeUserRepository()
viewModel = UserViewModel(repository)
}
@Test
fun `loadUser updates state to success`() = runTest {
repository.userToReturn = User("1", "Alice")
viewModel.loadUser("1")
val state = viewModel.state.value
assertTrue(state is UiState.Success)
assertEquals("Alice", (state as UiState.Success).user.name)
}
}
10. 性能(せいのう)最適化(さいてきか)
10.1 SwiftUI最適化
struct GoodView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<1000) { i in
ExpensiveView(index: i)
}
}
}
}
}
struct ExpensiveView: View, Equatable {
let data: ComplexData
static func == (lhs: ExpensiveView, rhs: ExpensiveView) -> Bool {
lhs.data.id == rhs.data.id
}
var body: some View {
// ...
}
}
10.2 Compose最適化
@Stable
data class UiState(
val items: List<Item>,
val isLoading: Boolean
)
@Composable
fun ExpensiveCalculation(items: List<Item>) {
val sortedItems = remember(items) {
items.sortedBy { it.priority }
}
LazyColumn {
items(sortedItems, key = { it.id }) { item ->
ItemRow(item)
}
}
}
11. リリースプロセス
11.1 iOS App Store
- Xcode CloudまたはGitHub ActionsでCI/CD
- TestFlightベータ配布(はいふ)
- App Store Connect (メタデータ、IAP、Privacy Manifest必須(ひっす))
- App Review (24-48時間)
- Phased Release (7日にわたる段階配布)
11.2 Google Play
- Gradle Play PublisherまたはFastlane
- Internal → Closed → Open → Production
- Play Console (App Bundle .aab必須、Data Safety Form、Target API Level 34+)
- Review (数時間〜数日)
- Staged Rollout
12. 比較(ひかく)まとめ表
| 項目(こうもく) | iOS | Android |
|---|---|---|
| 言語(げんご) | Swift 6 | Kotlin 2.0 |
| UIフレームワーク | SwiftUI 6 | Jetpack Compose 1.7 |
| 非同期 | async/await + actor | coroutines + Flow |
| DI | Swinject | Hilt |
| テスト | XCTest, Swift Testing | JUnit, Compose Testing |
| ビルド | Xcode, SPM | Gradle |
| リリース | App Store Connect | Play Console |
13. クイズ
Q1. Swift 6のstrict concurrencyが解決(かいけつ)する主(おも)な問題(もんだい)は?
A1. データ競合(きょうごう)をコンパイル時に検出(けんしゅつ)。actorとSendableプロトコルにより並行性安全(あんぜん)なコードを強制(きょうせい)し、ランタイムクラッシュを未然(みぜん)に防(ふせ)ぐ。
Q2. SwiftUIの@Stateと@Bindingの違い(ちがい)は?
A2. @StateはView内部(ないぶ)で所有(しょゆう)するローカル状態、@Bindingは親(おや)から渡(わた)された状態への双方向(そうほうこう)参照(さんしょう)。@Bindingにより子(こ)Viewが親の状態を変更(へんこう)できる。
Q3. Jetpack Composeのstate hoistingとは?
A3. 状態をcomposable関数内ではなく呼(よ)び出(だ)し側(parent)に引(ひ)き上(あ)げるパターン。これによりcomposableをstatelessにし、再利用性(さいりようせい)とテスト容易性(よういせい)を高(たか)め、単方向(たんほうこう)データフロー(UDF)を実現(じつげん)する。
Q4. Kotlin Coroutinesの構造化並行性(こうぞうかへいこうせい)とは?
A4. 親コルーチンが子コルーチンのライフサイクルを管理する原則(げんそく)。親がキャンセルされると子もキャンセルされ、子が全(すべ)て完了(かんりょう)してから親も完了する。coroutineScope、viewModelScopeなどがこれを実装(じっそう)する。
Q5. TCAの核心概念(かくしんがいねん)3つは?
A5. 1) State - 機能の状態を表(あらわ)す単一構造体、2) Action - 発生(はっせい)し得る全てのイベントを表すenum、3) Reducer - 現在(げんざい)のStateとActionを受(う)け取(と)り新しいStateとEffectを返(かえ)す純粋(じゅんすい)関数。単方向データフローとテスト可能性が核心。
14. 参考(さんこう)資料(しりょう)
- Apple - Swift.org Documentation: https://swift.org/documentation
- Apple - SwiftUI Tutorials: https://developer.apple.com/tutorials/swiftui
- Apple - Swift Concurrency: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency
- Apple - Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines
- Google - Jetpack Compose Documentation: https://developer.android.com/jetpack/compose
- Google - Kotlin Documentation: https://kotlinlang.org/docs
- Google - Android Architecture Guide: https://developer.android.com/topic/architecture
- JetBrains - Kotlin Multiplatform: https://kotlinlang.org/docs/multiplatform.html
- Point-Free - The Composable Architecture: https://github.com/pointfreeco/swift-composable-architecture
- Swift Package Manager: https://swift.org/package-manager
- Hilt - Dependency Injection for Android: https://dagger.dev/hilt
- Material Design 3: https://m3.material.io
- Google I/O 2024 Sessions: https://io.google
- WWDC 2024 Sessions: https://developer.apple.com/wwdc24
15. おわりに
ネイティブ開発は2026年でも依然(いぜん)としてモバイルアプリの黄金(おうごん)標準(ひょうじゅん)である。Swift 6のコンパイル時安全性とKotlin 2.0のK2コンパイラ性能改善は両(りょう)エコシステムをより強力(きょうりょく)にしている。SwiftUIとJetpack Composeは宣言型UIのパラダイムを完全(かんぜん)に定着(ていちゃく)させ、非同期処理、DI、テストの全般(ぜんぱん)で両プラットフォームが互(たが)いの良(よ)い手法を素早(すばや)く吸収(きゅうしゅう)している。
クロスプラットフォームが全ての要件(ようけん)を満(み)たせない領域(りょういき)が明確(めいかく)に存在(そんざい)する。高性能グラフィックス、最新OS統合(とうごう)、プラットフォーム特化(とっか)UXが必要なアプリならネイティブが正解(せいかい)である。