はじめに
ここではSwiftでのIOSアプリ作成の基本をまとめる。雛形はXcodeで提供されており、Hello, Worldを表示するものが提供されている(以下写真は初期状態のままビルド、テストしたもの)。本記事では、都度初歩的な疑問を解消しながら雛形を解説し、最低限いじれるように操作をまとめ、その結果を共有しておく。
テンプレのファイル群の解説
Create New Project...
よりChoose a template for your new project:
をクリックし、IOS
タブからApp
を選ぶ。そして必要な情報(プロジェクト名や言語等)を入れる。
ここで得られる雛形は"Hello, world!"画面を表示するだけのアプリとなっている。リポジトリの構成は以下の通り。プロジェクト名はhoge
とする。それぞれのファイルは何をやっているのだろうか?というか実行ボタンを押したらどのファイルがどういう順序で呼び出されるのだろうか?
hoge
├── hoge
├── hogeApp.swift
├── ContentView.swift
├── Assets
├── Preview Content
├── Preview Assets
├── hogeTests
├── hogeTests.swift
├── hogeUITests
├── hogeUITests.swift
├── hogeUITestsLaunchTests.swift
順番1. hogeApp.swift
最初に呼び出されるのは、hoge/hoge/ディレクトリ以下のhogeApp.swift
である。このファイルの雛形は以下のようになっている。
import SwiftUI
@main
struct hogeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
このコードで最も大事なのは@mainアノテーションが付いていることと、構造体がApp
プロトコルに準拠していることである。別に構造体の名前はなんでもいいし、構造体でなくてもいい(クラスとかでもワークする)。
import SwiftUI
@main
class Unchi: App {//名前をウンチ(Unchi)に、型をクラスに変更
required init() {}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@mainアノテーションが付与された型は、アプリケーションのエントリポイントとして機能する。
Q. App
プロトコルって何? てかプロトコルって何?
Swiftにおいてプロトコルとは、型が満たすべき制約を定めて型のインタフェースを決めるものである。
これにより共通の性質を抽象化できる。以下に例を示す
//プロトコル
protocol Animal {
func makeSound()
}
//プロトコルに準拠した型A
class Dog: Animal {
func makeSound() {
print("Woof!")
}
}
//プロトコルに準拠した型B
struct Cat: Animal {
func makeSound() {
print("Meow!")
}
}
//型は違えど共通の処理を一括で表現可能
let animals: [Animal] = [Dog(), Cat()]
for animal in animals {
animal.makeSound()
}
//プロトコル
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
//Intという型
extension Int: Equatable {
public static func == (lhs: Int, rhs: Int) -> Bool {
return lhs == rhs
}
}
//Stringという型
extension String: Equatable {
public static func == (lhs: String, rhs: String) -> Bool {
return lhs == rhs
}
}
//型は違えど共通の処理を表現可能
print(3 == 4)//Int型
print("abc" == "bcd")//String型
これらの例のように、プロトコルは、型に対して制約を求め、それに準拠した型が仮に異なる型であったとしても、共通の処理を行えるようにするためのものである。
Q. App
プロトコルって何?
さてここで、Appプロトコルは型に何を求めるのだろう? 結論、Appプロトコルはbodyプロパティ
を有しており、body
プロパティはSwiftUI
が用意しているScene
プロトコルに準拠した何かしらの型を返すことを求めている。
import SwiftUI
@main
struct hogeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
再掲だが、雛形のファイルではbody
プロパティの型の部分にsome Scene
とある。これはScene
プロトコルに準拠した何かしらの型(some
)という意味である。
その後に{}
が続いていて一瞬戸惑うが、これはコンピューテッドプロパティ
というやつである。
Q. コンピューテッドプロパティって何?
Swiftのプロパティには値を保持するストアドプロパティと、値を保持しないコンピューテッドプロパティがある。コンピューテッドプロパティは、GetterとSetterを有している。Getterは他のストアドプロパティなどから値を取得して、値を返す。Setterでは暗黙的に使用可能なnewValue
の値がGetterの返り値となるように値の更新が行われる。
struct length {
var meter: Double = 0.0
// "centimeter"はコンピューテッドプロパティ
var centimeter: Double {
get {
return 100 * meter
}
set {
meter = 0.01 * newValue
}
}
}
print(computed_var.meter) // 0.0
print(computed_var.centimeter) // 0.0
computed_var.meter = 2.5
print(computed_var.meter) // 2.5
print(computed_var.centimeter) // 250 (!meterの値が更新され、centimeterも更新)
computed_var.centimeter = 150
print(computed_var.meter) // 1.5
print(computed_var.centimeter) //150 (!centimeterの値が更新された場合、Setterによりmeterも自動で更新)
また、現実的によく見る例としてSetterがない場合も多く、この場合にはGetter関数自体も端折られる。
struct Rectangle {
var width: Double
var height: Double
var area: Double {
return width * height
}
}
var rect = Rectangle(width: 5, height: 10)
print(rect.area) // 50.0
rect.width = 7
print(rect.area) // 70.0 (!勝手にwidthと紐づいたareaプロパティも更新されている)
ContentView.swiftにおいては、bodyプロパティがコンピューテッドプロパティである。このプロパティはストアドプロパティでないのは当然で、例えば”ボタンを押す”や”スクロールをする”などのSwiftUIで管理される状態が変化するたびに、ビューを都度反映させる必要がある。そのためbodyプロパティはコンピューテッドプロパティでなければならない。
Q. WindowGroup
って何?
複数Windowが存在する場合、その切り替えを制御したりできるSwiftUIが用意してくれている構造体。一つのWindowしかないアプリを開発する場合がIOSアプリ開発では多い気がするが、これを省略するとAppプロトコルに準拠できなくなる。(ContentView
はSwiftUIが提供するView
プロトコルに準拠しているが、Scene
プロトコルには準拠していない。)
長くなるので具体的なコードは端折るが、Window制御は例えば以下のように「New Window」ボタンを押したら新Windowが開く形で実装される。
import SwiftUI
@main
struct hogeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("Window") {
Button("New Window") {
// 新しいウィンドウを開くアクション
}
}
}
}
}
Q. Scene
プロトコルとView
プロトコルって何?
Sceneプロトコルはアプリケーションのライフサイクル管理と主要なUI構成要素の定義に使用され、ウィンドウやウィンドウグループを管理するコンテナとして機能する。
Viewプロトコルは個々のUIコンポーネントや画面を定義するために使用される(例えばText、Button、ContentViewなど)。これによりユーザーインターフェースの構築に使用する(詳しくはContentViewにて)。
順番2. ContentView.swift
構造体の名前は別にContentView
でなくても良い(hogeApp.swift
での呼び出し名を変えれば)。この構造体はViewプロトコルに準拠してさえいれば良い。Viewプロトコルは、body
プロパティを一つ有していなければならない。これが唯一のビュー定義となる(body2
とかはあってはならない)。
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}
bodyプロパティでは、雛形ではVStack
というSwiftUIが提供してくれている構造体が定義されている。VStack {..}.padding()
とあるが、これはVStack全体に余白を発生させており、VStackで表示させるオブジェクトの好きな場所に余白を生成する(要は別に無くても問題ない)。例えば
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()//VStackの周りに均等にデフォルトサイズ分の余白
.background(Color.red)
.padding(.vertical, 20)//上下だけ20ポイントの余白
.background(Color.yellow)
.padding(.horizontal, 20)//左右だけ20ポイントの余白
.background(Color.green)
}
}
のようにpaddingをいじると以下のような誰得Hello Worldも生成できる(paddingの使い方はこういうことではないと思うが)。
ちなみにVStack
以外にもHstack
は横方向にオブジェクトを並べたり、Zstack
はオブジェクトを重ねて表示させたり、List
やForm
など様々な様式がある。
Q. bodyプロパティの中で使えるものは?
- Text
- Image
- Button
- Spacer
- Color
等様々。例えば、Imageメソッドでは自分で撮った画像を使うこともできるし、Buttonボタンを押したらテキストを表示するようなコードも以下のように作成することができる(以下)。
struct ContentView: View {
@State private var showStartText = false
var body: some View {
if showStartText {
Text("Done!")
}
Button("Push the Button"){
showStartText = true
}
}
}
他にも以下のように背景色が目に優しい誰得Hello Worldも生成できる。
struct ContentView: View {
var body: some View {
ZStack {
Color.green // 背景色を緑色に設定
.edgesIgnoringSafeArea(.all) // 全画面に色を適用
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
}
}
}
他にもたくさんファイルがあったけど?
ここまでhogeApp.swift
とContentView.swift
を扱ったが、主に操作するファイルはこの二つ(あるいはこいつらが呼び出す雛形とは別の新規作成ファイル)となる。他のファイルも一応解説しておく。
-
Assets
:ここに画像をアップロードして、自前の画像を使ったりできる。他にもカスタムColorを定義できたり、JSONやXMLなどのデータファイルを管理できる。例えば自分で用意した画像データをXcodeのAssets上にドラッグ&ドロップでアップロードして、以下のようなZStackを用いることで、ガッキーHello World UIが生成できる。
struct ContentView: View {
var body: some View {
ZStack {
Image("gakkii")
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
}
}
}
-
Preview Content
:プレビュー用のコンテンツを管理するためのディレクトリ。 -
hogeTests
:ユニットテスト(コンポーネントの動作を確認するために行う)を定義。アプリの動作を確認するためのテストコードが含まれる。 -
hogeUITests
:UIテスト(UIが正しく表示されるかを確認するために行う)を定義。アプリのUIを確認するためのテストコードが含まれる。 - (雛形にはGUI上では表示されていないが)
info.plist
これはクソ大事。権限まわりを設定する。例えばカメラアクセスを必要とするアプリではその旨を追記する。