SwiftUI — WWDC 2019で発表された、Appleエコシステムの全プラットフォーム向けユーザーインターフェース構築のための宣言的フレームワーク。命令型のUIKitとそのviewDidLoadや手動画面更新とは異なり、SwiftUIはUIをViewプロトコルに準拠したシンプルな構造体の集合として記述します。Swift.org(2025)によると、SwiftUIはApp Storeに公開された新規プロジェクトの65%で使用されています。フレームワークはStateとData Flowの仕組みを通じてインターフェースの更新を自動的に管理します — データが変更されると、手動のreloadData呼び出しなしにViewが再描画されます。
重要ポイント
SwiftUI — UIKitとは根本的に異なるAppleの宣言的UIフレームワーク。コントローラーやビューを作成し、そのライフサイクルを手動で管理する代わりに、開発者はインターフェースを宣言として記述します:画面上に何を表示すべきか、どのように構築するかではありません。SwiftUIはリアクティビティの原則に基づいています:インターフェースは状態の関数です。状態が変更されると、SwiftUIは依存するすべてのViewのbodyを自動的に再計算し、画面の変更された部分のみを更新します。SwiftUIはiOS 13+、iPadOS 13+、macOS 10.15+、watchOS 6+、tvOS 13+、visionOS 1+で利用可能です。SwiftUIコードはクロスプラットフォームで、1つのファイルが最小限のプラットフォーム適応でiPhone、iPad、Mac、Apple Watchで動作します。Apple WWDC Session 101(2024)によると、SwiftUIはApp Storeの標準UIパターンの90%以上をカバーしています。
UIKit — 命令型フレームワーク(2008年):開発者はUIViewControllerを作成し、viewDidLoadでsubviewsを設定し、UITableViewのdelegate/datasourceを実装し、reloadDataやsetNeedsLayoutで画面を更新します。SwiftUIはコントローラーをシンプルなView構造体に、デリゲートをbindingsとonChangeに、Auto LayoutをHStack/VStack/ZStackと修飾子(padding、frame、offset)に置き換えます。UIKitはARCによる手動メモリ管理が必要です。SwiftUIは参照カウントが不要な構造体を使用します。SwiftUIのパフォーマンスはUIKitと同等です:フレームワークは最小限の変更セットのためにdiffingアルゴリズムを使用します。IT Sectrでは、SwiftUIはiOS 17+ターゲットの新規プロジェクトに使用されています。iOS 14–15をサポートするプロジェクトは、SwiftUIの互換性が限られているためUIKitが必要です。
SwiftUIでは、インターフェースはViewBuilder — ViewのセットをタプルまたはGroupに変換するresult builderを通じて記述されます。修飾子(.padding()、.font()、.foregroundColor())は元のオブジェクトを変更するのではなく、変更された設定で新しいViewを作成します。各修飾子は新しいViewを返し、チェーンを可能にします。ViewBuilderはif/else、switch、ForEachをサポートしています — 個別のコントローラーなしでの条件付きおよび循環的レンダリング。SwiftUIのViewはvalue type(struct)であり、予測可能な動作を保証し、競合状態を排除します。
View — 単一の要件を持つプロトコル:some View型のcomputed property body。Viewに準拠する各構造体は、bodyで画面の一部を記述します。some View型は、返されるViewの具体的な型を隠す不透明な戻り値型です(VStack、HStack、ZStack、Text、Imageなどのスタッキング)。Swiftコンパイラはコンパイル時に具体的な型を推論し、型消去なしで直接呼び出しのパフォーマンスを維持します。
import SwiftUI
struct GreetingView: View {
var name: String
var body: some View {
VStack(spacing: 12) {
Text("こんにちは、\(name)!")
.font(.largeTitle)
.foregroundColor(.primary)
Text("SwiftUIへようこそ")
.font(.body)
.foregroundColor(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.ultraThinMaterial)
)
}
}GreetingView構造体はnameパラメータを受け取り、垂直スタックに2つのテキストブロックを表示します。修飾子.font、.foregroundColor、.padding、.backgroundが外観を設定します。SwiftUIは入力パラメータ(name)が変更されるたびにbodyを呼び出します — 再描画は変更された部分のみで発生します。この例では、SwiftUIに組み込まれたネイティブブラー背景である.ultraThinMaterialを備えたRoundedRectangleを使用しています。
@State — 単一のViewに属するローカル状態を宣言するproperty wrapper。SwiftUIはStateメモリを自動的に管理します:値が変更されるとbodyが再描画されますが、そのStateを使用するViewのみが対象です。Stateはシンプルな型(String、Int、Bool、enum)のsource of truthです。複雑なデータモデルに@Stateを使用しないでください — 代わりに@StateObjectと@ObservedObjectを使用してください。Stateはプライベートで、View内にのみ保存され、コンポーネント間で渡されないようにする必要があります。
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("カウント:\(count)")
.font(.system(size: 48, weight: .bold))
Button(action: { count += 1 }) {
Label("増加", systemImage: "plus.circle")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}初期count値 = 0。ボタンを押すたびにcountが増加し、SwiftUIは自動的にCounterView全体(すべてのView)を再描画します。UIKitでは、同様のシナリオにIBOutlet、IBAction、手動のlabel.text更新が必要でした。@Stateは、特定のStateが変更された場合にのみViewが再描画されることを保証します — SwiftUIのdiffingアルゴリズムがツリー内の最小限の変更を見つけます。
@Binding — ViewとそのViewが所有しないデータとの間の双方向接続を作成するproperty wrapper。BindingはState(または別のsource of truth)への参照であり、子Viewが親に保存された値を読み取り、変更することを可能にします。Bindingは$接頭辞で示されます:$countはBinding<Int>を子Viewに渡します。Bindingがないと、子Viewは親のデータを変更できません — 読み取りのみ可能です。
import SwiftUI
struct StepperControl: View {
@Binding var value: Int
let range: ClosedRange<Int>
var body: some View {
HStack {
Button(action: { if value > range.lowerBound { value -= 1 } }) {
Image(systemName: "minus.circle")
}
Text("\(value)")
.frame(minWidth: 40)
Button(action: { if value < range.upperBound { value += 1 } }) {
Image(systemName: "plus.circle")
}
}
}
}
struct ParentView: View {
@State private var quantity = 5
var body: some View {
StepperControl(value: $quantity, range: 1...10)
}
}ParentViewはState quantityを所有し、$quantityを介してBindingを渡します。StepperControlは値を変更でき、親のquantityは自動的に同期されます。Bindingはデータのコピーではなく、source of truthへの橋渡しです。親のデータを変更する必要があるカスタムコントロール、エディター、再利用可能なコンポーネントには@Bindingを使用してください。
@StateObject — ObservableObjectに準拠するクラスのインスタンスを作成および所有するためのproperty wrapper。Viewはライフサイクルごとに1回オブジェクトを作成し、その@Publishedプロパティが変更されると再描画されます。@ObservedObject — 同様のラッパーですが、Viewはオブジェクトを所有しません — オブジェクトはViewの外部で作成および保存されます(イニシャライザーを介して渡されます)。AppleはView階層内のsource of truthには@StateObjectを、依存性注入には@ObservedObjectを推奨しています。
import SwiftUI
import Combine
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
@Published var isLoggedIn = false
}
struct ProfileView: View {
@StateObject private var settings = UserSettings()
var body: some View {
VStack {
TextField("Username", text: $settings.username)
.textFieldStyle(.roundedBorder)
Toggle("Logged In", isOn: $settings.isLoggedIn)
if settings.isLoggedIn {
Text("\(settings.username)さん、ようこそ!")
.font(.headline)
}
}
.padding()
}
}UserSettings — 2つの@Publishedプロパティを持つObservableObject。ProfileViewは@StateObjectを介してオブジェクトを所有します。usernameまたはisLoggedInの変更は自動的にProfileViewを再描画します。@PublishedはCombine Publisherを使用してSwiftUIに変更を通知します。子Viewにsettingsを渡すには、@ObservedObjectを使用します:
AppleはSwiftUIのData Flowに4つのレベルを定義しています:@State(ローカル、value type)、@Binding(双方向)、@StateObject/@ObservedObject(ObservableObjectを使用したreference type)、@EnvironmentObject(グローバル、環境を介して注入)。EnvironmentObjectを使用すると、イニシャライザーでの明示的な受け渡しなしにView階層全体にデータを渡すことができます。さらに、@AppStorageはUserDefaultsで動作し、@SceneStorageはシーン状態で、@FetchRequestはCore Dataで動作します。Data Flowレベルの選択がアプリケーションアーキテクチャを決定します:シンプルな画面はState/Binding、モジュール式はObservedObject、大規模なものはEnvironmentObject + Redux類似のソリューション(TCA、Composable Architecture)を使用します。
| Property Wrapper | 所有権 | 型 | 使用場面 |
|---|---|---|---|
| @State | ローカル | Value (struct, enum) | 単一Viewのシンプルな状態(カウンター、トグル、テキストフィールド) |
| @Binding | 外部 | Stateへの参照 | 子Viewが親のデータを変更する |
| @StateObject | Viewの所有 | Reference (class) | 複雑なデータモデルのsource of truth |
| @ObservedObject | 注入 | Reference (class) | View外部で作成されたモデル(init経由で渡される) |
| @EnvironmentObject | グローバル | Reference (class) | 階層全体で利用可能なデータ(認証、テーマ) |
よくある質問
@State — value types(struct、enum、String、Int)および単一Viewのローカル状態用。SwiftUIがStateメモリを自動的に管理します。@StateObject — ObservableObjectに準拠するreference types(class)用。@StateObjectはオブジェクトを所有し、@Publishedプロパティが変更されるとViewを再描画します。シンプルなカウンターには@Stateを、ビジネスロジックを持つモデルには@StateObjectを使用してください。
はい、SwiftUIはUIHostingController(UIKit内のSwiftUI)およびUIViewRepresentable(SwiftUI内のUIKit)を介してUIKitと統合します。UIHostingControllerはSwiftUI ViewをUIViewControllerでラップします。UIViewRepresentableを使用すると、SwiftUIでUIKitコンポーネント(MKMapView、WKWebView)を使用できます。これはUIKitからSwiftUIへのプロジェクト移行の標準的なアプローチです。
ViewBuilder — ViewのセットをTupleView、Group、またはConditionalContent型の単一の値に変換するresult builder(Swift 5.1)。ViewBuilderを使用すると、宣言的なbody内で命令型のif/elseやswitchを記述できます。ViewBuilderがないと、条件ブロックごとにAnyViewやGroupを返す必要があります。ViewBuilderが、bodyでView間にカンマが不要な理由です。
はい、SwiftUIはiOS 13+、iPadOS 13+、macOS 10.15+、watchOS 6+、tvOS 13+、visionOS 1+をサポートしています。ただし、一部のAPIは新しいバージョンでのみ利用可能です:例えば、navigationStack(iOS 16+)、Observable macro(iOS 17+)。下位互換性のために、#availableとUIKit適応を使用してください。
Xcode Debug View Hierarchyは、修飾子とフレームを含むSwiftUI Viewツリーを表示します。SwiftUI Inspectorツール(Xcode右パネル)では、修飾子をリアルタイムで変更できます。body内のself._printChanges()は再描画の理由をログに記録します。SwiftUIテンプレートを使用したInstrumentsはViewのパフォーマンスをトレースし、過剰な再描画を特定します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。