メモ > 技術 > IDE: Xcode > SwiftUI
SwiftUI
■考察
「SwiftUIでMVVMを採用するのは止めよう」と思い至った理由 #Swift - Qiita
https://qiita.com/karamage/items/8a9c76caff187d3eb838
■初期コード
SwiftUIでの初期コードは以下のとおり
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world.")
}
.padding()
}
}
#Preview {
ContentView()
}
Xcode15より前は、以下のようになっていた
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
■プレビューのサイズを変更
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world.")
}
.padding()
}
}
#Preview(traits: .fixedLayout(width: 100, height: 100)) {
ContentView()
}
上記のように「#Preview」の引数を指定するらしい
ただしXcode16時点で試しても、プレビューの表示に変化は見られなかった
(一覧表示するプログラムを作る際などに、一行だけのプレビューを表示する…のような場合に使うのかもしれない)
Xcode 15: SwiftUI preview layout: size that fits does not work - Stack Overflow
https://stackoverflow.com/questions/77167973/xcode-15-swiftui-preview-layout-size-that-fits-does-not...
Xcode15より前は、以下のように「previewLayout」を使用していた
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 200, height: 80))
}
}
以降では、原則「ContentView」の内容のみを記載する
■横に並べて表示
ContentView.swift
struct ContentView: View {
var body: some View {
HStack {
Text("AAA")
Text("BBB")
Text("CCC")
}
}
}
以下のように spacing を指定すると、要素間に余白を設けることができる
HStack (spacing: 20) {
Text("AAA")
Text("BBB")
Text("CCC")
}
■縦に並べて表示
ContentView.swift
struct ContentView: View {
var body: some View {
VStack {
Text("AAA")
Text("BBB")
Text("CCC")
}
}
}
以下のように spacing を指定すると、要素間に余白を設けることができる
VStack (spacing: 20) {
Text("AAA")
Text("BBB")
Text("CCC")
}
■スペーサー
ContentView.swift
struct ContentView: View {
var body: some View {
VStack {
Text("AAA")
Spacer()
Text("BBB")
Spacer()
Text("CCC")
}
}
}
■余白
padding() で余白を設定できる
padding(20) で余白のサイズを指定できる
.padding(.vertical, 100) で特定の方向の余白を指定できる
EdgeInsets() を組み合わせることで、上下左右を個別に指定することもできる
Text("Memo")
.padding(EdgeInsets(
top: 8,
leading: 0,
bottom: 0,
trailing: 0
))
【SwiftUI】Viewに余白を付加する(padding) | カピ通信
https://capibara1969.com/1954/
【SwiftUI】余白を追加するModifier「padding」の使い方まとめ | Swift Note
https://naoya-ono.com/swift/swiftui-modifier-padding/
■フォントの指定
ContentView.swift
struct ContentView: View {
var body: some View {
VStack (spacing: 10) {
Text("largeTitle").font(.largeTitle)
Text("title").font(.title)
Text("headline").font(.headline)
Text("subheadline").font(.subheadline)
Text("body").font(.body)
Text("callout").font(.callout)
Text("footnote").font(.footnote)
Text("caption").font(.caption)
}
}
}
■テキストの装飾
ContentView.swift
struct ContentView: View {
var body: some View {
VStack (spacing: 10) {
// 文字数を制御
Text("これはサンプルですこれはサンプルです").font(.title).frame(width: 300, height: 50).truncationMode(.middle)
// 行数を制御
Text("これはサンプルですこれはサンプルですこれはサンプルですこれはサンプルですこれはサンプルです").lineLimit(2)
// 文字の色を制御
Text("これはサンプルです").foregroundColor(.red)
// 文字の太さを制御
Text("これはサンプルです").fontWeight(.bold)
// 文字の下線を制御
Text("これはサンプルです").underline()
// 文字の打ち消し線を制御
Text("これはサンプルです").strikethrough()
// 文字の間隔を制御
Text("これはサンプルです").kerning(3)
}
}
}
■ボタンの表示
ContentView.swift
struct ContentView: View {
var body: some View {
Button(action: {
print("ボタンが押されました")
}) {
Text("ボタン")
}
}
}
■ボタンの装飾
ContentView.swift
struct ContentView: View {
var body: some View {
Button(action: {
print("ボタンが押されました")
}) {
VStack {
// 画像を配置
Image(systemName: "camera")
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
// 色を指定してテキストを配置
Text("ボタン")
.foregroundColor(.black)
}
// タップ領域を広く
.frame(width: 150, height: 100)
// 角丸の枠線を付ける
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.black, lineWidth: 2)
)
}
}
}
■リスト表示
ContentView.swift
struct ContentView: View {
var body: some View {
List {
Text("コンテンツ1")
Text("コンテンツ2")
Text("コンテンツ3")
Text("コンテンツ4")
Text("コンテンツ5")
}
}
}
以下はアイコンとともにリスト表示する例
ContentView.swift
struct ContentView: View {
var body: some View {
List {
HStack {
Image(systemName: "moon")
Text("moon")
}
HStack {
Image(systemName: "sun.max")
Text("sun")
}
HStack {
Image(systemName: "cloud")
Text("cloud")
}
}
}
}
■画像と独自ビューをリスト表示
ContentView.swift
struct ContentView: View {
struct PhotoSample: View {
var body: some View {
HStack {
Image("caramel")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
.clipShape(Circle())
.overlay(
Text("ハロー!")
//.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
//.offset(x: 0, y: -50)
.shadow(radius: 2)
)
}
}
}
var body: some View {
List {
Text("コンテンツ1")
Text("コンテンツ2")
Image("brownie")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Text("コンテンツ3")
PhotoSample()
Text("コンテンツ4")
Text("コンテンツ5")
}
}
}
■配列をリスト表示
ContentView.swift
struct ContentView: View {
var body: some View {
let metro = [
"銀座線",
"丸ノ内線",
"日比谷線",
"東西線",
"千代田線",
"半蔵門線",
"南北線",
"副都心線",
]
List(0 ..< metro.count) { item in
HStack {
Text(String(item))
Text(metro[item])
}
}
}
}
■配列を複数のセクションでリスト表示
ContentView.swift
struct ContentView: View {
var body: some View {
let shikoku = [
"徳島県",
"香川県",
"愛媛県",
"高知県",
]
let kyushu = [
"福岡県",
"佐賀県",
"長崎県",
"熊本県",
"大分県",
"宮崎県",
"鹿児島県",
]
NavigationView {
List {
Section(header: Text("四国"), footer: Text("四国の都道府県一覧")) {
ForEach(0 ..< shikoku.count) { index in
Text(shikoku[index])
}
}
Section(header: Text("九州"), footer: Text("九州の都道府県一覧")) {
ForEach(0 ..< kyushu.count) { index in
Text(kyushu[index])
}
}
}
.navigationTitle("タイトル")
.navigationBarTitleDisplayMode(.inline)
.listStyle(GroupedListStyle())
}
}
}
■簡単なナビゲーションリンク
ContentView.swift
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SubView()) {
Text("サブビューへ移動")
.padding()
}
.navigationTitle("ホーム")
}
}
}
struct SubView: View {
var body: some View {
Text("サブビュー")
}
}
■写真の一覧と詳細を表示
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List(photoArray) { item in
NavigationLink(destination: PhotoDetailView(photo: item)) {
RowView(photo: item)
}
}
.navigationTitle(Text("写真リスト"))
}
}
}
#Preview {
ContentView()
}
PhotoDetailView.swift
import SwiftUI
struct PhotoDetailView: View {
var photo: PhotoData
var body: some View {
VStack {
Image(photo.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
Text(photo.title)
Spacer()
}
.padding()
// タイトル
.navigationTitle(Text(verbatim: photo.title))
.navigationBarTitleDisplayMode(.inline)
}
}
struct PhotoDetailView_Previews: PreviewProvider {
static var previews: some View {
PhotoDetailView(photo:photoArray[0])
}
}
RowView.swift
import SwiftUI
struct RowView: View {
var photo: PhotoData
var body: some View {
HStack {
Image(photo.imageName)
.resizable()
.frame(width: 110, height: 80)
Text(photo.title)
Spacer()
}
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(photo:photoArray[0])
.previewLayout(.fixed(width: 300, height: 80))
}
}
PhotoData.swift
import Foundation
// 写真データを配列に入れる
var photoArray: [PhotoData] = makeData()
// 写真データを構造体で定義する
struct PhotoData: Identifiable {
var id: Int
var imageName: String
var title: String
}
// 構造体PhotoData型の写真データが入った配列を作る
func makeData()->[PhotoData] {
var dataArray: [PhotoData] = []
dataArray.append(PhotoData(id: 1, imageName: "photo01", title: "写真1"))
dataArray.append(PhotoData(id: 2, imageName: "photo02", title: "写真2"))
dataArray.append(PhotoData(id: 3, imageName: "photo03", title: "写真3"))
dataArray.append(PhotoData(id: 4, imageName: "photo04", title: "写真4"))
dataArray.append(PhotoData(id: 5, imageName: "photo05", title: "写真5"))
dataArray.append(PhotoData(id: 6, imageName: "photo06", title: "写真6"))
dataArray.append(PhotoData(id: 7, imageName: "photo07", title: "写真7"))
dataArray.append(PhotoData(id: 8, imageName: "photo08", title: "写真8"))
dataArray.append(PhotoData(id: 9, imageName: "photo09", title: "写真9"))
dataArray.append(PhotoData(id: 10, imageName: "photo10", title: "写真10"))
dataArray.append(PhotoData(id: 11, imageName: "photo11", title: "写真11"))
dataArray.append(PhotoData(id: 12, imageName: "photo12", title: "写真12"))
dataArray.append(PhotoData(id: 13, imageName: "photo13", title: "写真13"))
dataArray.append(PhotoData(id: 14, imageName: "photo14", title: "写真14"))
dataArray.append(PhotoData(id: 15, imageName: "photo15", title: "写真15"))
dataArray.append(PhotoData(id: 16, imageName: "photo16", title: "写真16"))
return dataArray
}
■タブビュー
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
Text("テスト")
.fontWeight(.bold)
.tabItem {
Image(systemName: "message")
Text("Message")
}
TextPage()
.tabItem {
Image(systemName: "iphone")
Text("iPhone")
}
ListPage()
.tabItem {
Image(systemName: "list.dash")
Text("List")
}
}
}
}
struct TextPage: View {
var body: some View {
Text("コンテンツ")
}
}
struct ListPage: View {
var body: some View {
List {
Text("コンテンツ1")
Text("コンテンツ2")
Text("コンテンツ3")
Text("コンテンツ4")
Text("コンテンツ5")
}
}
}
#Preview {
ContentView()
}
■スクロールビュー
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: true, content: {
VStack {
Text("コンテンツ1").frame(height: 100)
Text("コンテンツ2").frame(height: 100)
Text("コンテンツ3").frame(height: 100)
Text("コンテンツ4").frame(height: 100)
Text("コンテンツ5").frame(height: 100)
Text("コンテンツ6").frame(height: 100)
Text("コンテンツ7").frame(height: 100)
Text("コンテンツ8").frame(height: 100)
Text("コンテンツ9").frame(height: 100)
Text("コンテンツ10").frame(height: 100)
}
.frame(maxWidth: .infinity)
})
}
}
#Preview {
ContentView()
}
■アラート
ContentView.swift
import SwiftUI
struct ContentView: View {
@State var show: Bool = false
var body: some View {
Button(action: {
show = true
}) {
Text("Alertテスト")
}.alert(isPresented: $show, content: {
Alert(
title: Text("タイトル"),
message: Text("メッセージ"),
dismissButton: .default(Text("OK"), action: {
print("OKがタップされました")
})
)
})
}
}
#Preview {
ContentView()
}
■アクションシート
ContentView.swift
import SwiftUI
struct ContentView: View {
@State var show: Bool = false
var body: some View {
Button(action: {
show = true
}) {
Text("ActionSheetテスト")
}.actionSheet(isPresented: $show, content: {
ActionSheet(
title: Text("タイトル"),
message: Text("メッセージ"),
buttons: [
.default(Text("選択肢1"), action: {
print("選択肢1がタップされました")
}),
.default(Text("選択肢2"), action: {
print("選択肢2がタップされました")
}),
.default(Text("選択肢3"), action: {
print("選択肢3がタップされました")
})
]
)
})
}
}
#Preview {
ContentView()
}
■シート
iOS14から対応したもの
シート型のモーダル表示を行う
ContentView.swift
import SwiftUI
struct ContentView: View {
@State var show: Bool = false
var body: some View {
Button(action: {
show = true
}) {
Text("Sheetテスト")
}.sheet(isPresented: $show, content: {
Text("これはシートの内容です。").padding(10)
Button(action: {
show = false
}) {
Text("閉じる")
}.padding(10)
})
}
}
#Preview {
ContentView()
}
■リンク
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Link(destination: URL(string: "https://www.apple.com/jp/")!, label: {
HStack {
Image(systemName: "link")
Text("Apple")
}
})
Link(destination: URL(string: "https://www.google.com/?hl=ja")!, label: {
HStack {
Image(systemName: "link")
Text("Google")
}
})
Link(destination: URL(string: "https://www.microsoft.com/ja-jp")!, label: {
HStack {
Image(systemName: "link")
Text("Microsoft")
}
})
Link(destination: URL(string: "https://www.amazon.co.jp/")!, label: {
HStack {
Image(systemName: "link")
Text("Amazon")
}
})
}
}
}
#Preview {
ContentView()
}
■リードオンリーの変数
ContentView.swift
import SwiftUI
struct ContentView: View {
var test2: Int {
get {
5 * 3
}
}
var body: some View {
VStack {
let test1 = 10
Text("test1 = " + String(test1))
Text("test2 = " + String(test2))
Spacer()
}
.padding(.top, 50)
}
}
#Preview {
ContentView()
}
■プロパティ
データが値型で、Viewがデータを読み込みだけする場合、そのデータはプロパティで管理できる
読み込み用なので let で定義するといい
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
SampleView(color: .red)
SampleView(color: .green)
SampleView(color: .blue)
}
}
}
struct SampleView: View {
let color: Color
var body: some View {
Circle().foregroundColor(color)
}
}
#Preview {
ContentView()
}
■some
Opaque Result Typesのプロトコルに準拠した型であることを表す構文
Opaque Result Typesに準拠することで、内部実装を隠蔽しながらパフォーマンスにも影響させずに返り値の型を抽象化することができる
【SwiftUI】someってなに?詳しく説明 | S.T.Blog
https://shuhey-hashimoto.com/swiftui/swiftuisome%E3%81%A3%E3%81%A6%E3%81%AA%E3%81%AB%E7%B0%A1%E5%8D%...
SwiftUIに出てくるsomeとは何なのか | レコチョクのエンジニアブログ
https://techblog.recochoku.jp/7754
SwiftUIで何気なく使っている some を調べてみる - Qiita
https://qiita.com/masa7351/items/1e6b235c1c0d3f54b3de
■Combine
※勉強中メモ
イベントを発行する側と受け取る側に分かれて、あるイベントが発行されたら、それを受け取った側の処理が走ることを容易にしたフレームワーク
【Swift】Combine入門レベルのまとめ - Qiita
https://qiita.com/kamomeKUN/items/394e668ee8c2a0b3327a
【Swift】 Combineを使用するメリットについて考えてみる | レコチョクのエンジニアブログ
https://techblog.recochoku.jp/8537
Combine初心者講座 -SwiftUIの相棒を使いこなそう- - bravesoft blog
https://www.bravesoft.co.jp/blog/archives/15610
SwiftUIとCombineの基礎 - アドグローブブログ | 渋谷のIT会社
https://blog.adglobe.co.jp/entry/2022/05/13/100000
【Combine】Timerの処理をCombineを使って置き換える - Swift・iOS
https://www.hfoasi8fje3.work/entry/2021/08/22/%E3%80%90Combine%E3%80%91Timer%E3%81%AE%E5%87%A6%E7%90...
SwiftUIとCombineを使ったMVVMの実装 - トレタ開発者ブログ
https://tech.toreta.in/entry/2019/12/24/104612
Combine入門 | CombineでTimer処理を行う方法 | アールケー開発
https://www.rk-k.com/archives/3943
Combine入門 | CombineでNotificationを受け取る方法 | アールケー開発
https://www.rk-k.com/archives/3937
RxSwiftとは?導入方法と使い方まとめ!ストリームを理解する
https://tech.amefure.com/swift-rxswift
■@State
@State を付与したプロパティが更新されると、SwiftUIが自動的に関連するデータを更新する
外から渡されるデータでは無いので、private を付けておくといい
ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var counter = 0
var body: some View {
Button(action: {
counter += 1
}, label: {
Text("counter is \(counter)")
})
}
}
#Preview {
ContentView()
}
以下のように変数名に $ を付けると、TextFieldの値が更新されると自動でTextの表示も更新される
ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var inputText: String = ""
var body: some View {
VStack {
TextField("", text: $inputText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text(inputText)
}
}
}
#Preview {
ContentView()
}
■@Published
@State はViewごとに変数を宣言して使用する
@Published は ObservableObject クラスに準拠させたクラス内で定義することにより、複数のViewから使用することができる。その際、@ObservedObject で宣言して使用する
これにより、クラスの値が変更されると、ビューが自動的に再描画される
つまり @ObservedObject とは、@State を複数同時に宣言できるクラスのこと
【SwiftUI】ObservableObjectについて(クラス、入れ子、配列など) | thwork
https://thwork.net/2021/08/31/swiftui_observableobject/
【SwiftUI】@ObservedObjectの使い方を徹底解説 | iOS-Docs
https://ios-docs.dev/swiftui-observerdobjects/
SwiftUIにおけるObservableObjectの管理
https://zenn.dev/yorifuji/articles/swiftui-manage-observableobject
■@Binding
@State を付与したデータを子Viewに渡す場合、@Binding のデータとして渡す
@Binding は外からの値を受け取ることになるので、private にはしない
ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var counter = 0
var body: some View {
HStack {
SampleView(counter: $counter)
.frame(width: .infinity)
}
}
}
struct SampleView: View {
@Binding var counter: Int
var body: some View {
Button(action: {
counter += 1
}, label: {
Text("counter is \(counter)")
})
}
}
#Preview {
ContentView()
}
■@Environment
@Environment を付与すると、View の環境値を読み取ることができる
スクリーンの解像度や言語と地域情報などをが集められている
以下は端末がライトモードかダークモードかを読み取る方法
ContentView.swift
import SwiftUI
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme: ColorScheme // 「\」はバックスラッシュ
var body: some View {
if colorScheme == .dark {
Text("ダークモード")
} else if colorScheme == .light {
Text("ライトモード")
} else {
Text("不明")
}
}
}
#Preview {
ContentView()
}