はじめに
こんにちは@kaneko77です。
次からの案件がSwiftUI
を採用をしているみたいで、これまでUIKit
オンリーで書いていたので色々SwiftUI
の書き方をまとめました。
SwiftUI
という新しい土俵(?)に上がって少し不安もありますが、UIKIt
でのナレッジがあるのでどうにかなるはず....
私と同じようにこれまでUIKIt
オンリーでやってきたエンジニアさん多いと思います。
採用率増えてますし、Storyboard
消してコードだけでレイアウト組んでるプロダクトが大半ですもんね。
ということで共有初めていきます!!
こんな方対象
- これまで
UIKIT
でレイアウト組んでたけどSwiftUI
やることになった方 - SwiftUIについてチートシート的な感じで知っておきたい方
UIKitとSwiftUIの違い一覧
まずは一覧
引用
紹介
ちょっと先に余談
私の端末Xcode13 beta
なんですが、このXcodeからSwiftUI
で新しくプロジェクト作成するとinfo.plist
とかSceneDelegate.swift
とかないので注意
まだXcode13
の参考書とか出てるわけでもなく情報も少ないのでbetaでない方のXcodeでやることおすすめします。
一応なんで無くなったのって人のために説明 or 記事記事共有しておきます。
-
info.plist
が無くなった理由 参照 -
SceneDelegate
やAppDelegate
は前のバージョンではlifecycle
って項目が新規フォロジェクトを立ち上げるとあったのですがその項目自体がなくなったから自動的にSwiftUI App
が適用されることになりました。参照
余談以上です。
Text編
定義の仕方
let testText: UILabel = {
let text = UILabel()
text.text = "おはよう世界!"
return text
}()
var body: some View {
Text("おはよう世界!")
}
文字色の変更
let text = UILabel()
text.text = "おはよう世界!"
text.textColor = .red
Text("おはよう世界!")
.foregroundColor(.red)
背景色の変更
let text = UILabel()
text.text = "おはよう世界!"
text.backgroundColor = .black
Text("おはよう世界!")
.background(Color.black)
余白
UiKit
では例えばUILabel
の余白を表示しようとなるとサブクラス化したりと面倒でした..
SwiftUI
からはこりゃまたびっくり!!
詳しくはこちら参照
Text("はひふへほ")
.padding(55)
.background(Color.red)
文字の太さの変更
let text = UILabel()
text.text = "おはよう世界!"
text.font = .boldSystemFont(ofSize: 20)
Text("おはよう世界!")
.bold()
.font(.system(size: 20))
複数行に対応する
let text = UILabel()
text.text = "おはよう\n世界!"
text.numberOfLines = 0
Text("おはよう\n世界!")
.lineLimit(nil)
画像編
let imageView: UIImageView = {
let image = UIImageView()
image.image = UIImage(systemName: "person.fill")
return image
}()
self.view.addSubview(imageView)
Image(systemName: "person.fill")
Label編
UIKit
では画像とテキストを一緒に表示する時、attribute
などで割と面倒くさく表現していました。
SwiftUI
では専用の物が用意されています。これまためちゃくちゃ簡単にできます。
画像はシンボルを使います。
Label("テストで表示", systemImage: "person.fill")
// Viewを入れることもできます
Label(
title: {
Text("色変更").foregroundColor(.red)
},
icon: {
Image(systemName: "person.fill").foregroundColor(.blue)
}
)
コンテンツの表示編
SwiftUI
でのコントロール(並べる)は最大10個まで
横に並べる
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
return stack
}()
let testText: UILabel = {
let text = UILabel()
text.text = "おはよう世界!"
return text
}()
let testText1: UILabel = {
let text = UILabel()
text.text = "こんにちは世界!"
return text
}()
addSubview(stackView)
stackView.addArrangedSubview(testText)
stackView.addArrangedSubview(testText1)
var body: some View {
HStack{
Text("おはよう世界!")
Text("こんにちは世界!")
}
}
縦に並べる
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
return stack
}()
let testText: UILabel = {
let text = UILabel()
text.text = "おはよう世界!"
return text
}()
let testText1: UILabel = {
let text = UILabel()
text.text = "こんにちは世界!"
return text
}()
addSubview(stackView)
stackView.addArrangedSubview(testText)
stackView.addArrangedSubview(testText1)
var body: some View {
VStack{
Text("おはよう世界!")
Text("こんにちは世界!")
}
}
親コンポーネントのレイアウト変更
表現が難しいですね。親コンポーネント=まとめているViewということです。
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.backgroundColor = .red
return stack
}()
let testText: UILabel = {
let text = UILabel()
text.text = "おはよう世界!"
return text
}()
let testText1: UILabel = {
let text = UILabel()
text.text = "こんにちは世界!"
return text
}()
addSubview(stackView)
stackView.addArrangedSubview(testText)
stackView.addArrangedSubview(testText1)
var body: some View {
VStack{
Text("おはよう世界!")
Text("こんにちは世界!")
}.background(Color.red)
}
繰り返し条件を使ってレイアウトを表示
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
return stack
}()
let testTexts: [UILabel] = {
let text = UILabel()
text.text = "hoge"
let text1 = UILabel()
text1.text = "huga"
let text2 = UILabel()
text2.text = "hugahoge"
return [text, text1, text2]
}()
addSubview(stackView)
testTexts.forEach{
stackView.addArrangedSubview($0)
}
let testTexts: [Text] = [
Text("hoge"),
Text("huga"),
Text("hugahoge")
]
var body: some View {
VStack{
ForEach(0 ..< testTexts.count){ self.testTexts[$0] }
}.background(Color.red)
}
区切り線
UIKit
だとこれUIView
で高さを1とかにして表現して作ってましたがSwiftUI
だと標準で用意されているみたいです。
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
return stack
}()
let testText: UILabel = {
let text = UILabel()
text.text = "hoge"
return text
}()
let testText1: UILabel = {
let text = UILabel()
text.text = "huga"
return text
}()
let spacer: UIView = {
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 1)
return view
}()
addSubview(stackView)
[testText, spacer, testText1].forEach{
stackView.addArrangedSubview($0)
}
var body: some View {
VStack{
Text("hoge")
Divider()
Text("huga")
}
}
コンテンツをView全体に表示する
UIKit
だとまずview
作ってそれにAutoLayout適用させて私は全体表示表現してました。
しかしこれまた簡単にできるものが用意されてます。
詳しくはこちら参照
struct ContentView: View {
private var colorView: some View {
Color.init(.red)
}
var body: some View {
colorView
.edgesIgnoringSafeArea(.all)
}
}
// 上記だったこんな風でいいです。(あくまでUIKit的に上記で書きました)
struct ContentView: View {
var body: some View {
Color.red
.edgesIgnoringSafeArea(.all)
}
}
Button編
呼び出し方
let testButton: UIButton = {
let button = UIButton()
button.setTitle("ボタン", for: .normal)
button.addTarget(
self,
action: #selector(tapped(_:)),
for: .touchUpInside
)
return button
}()
@objc func tapped(_ sender : Any) {
print("タップされた")
}
var body: some View {
VStack{
Button(action: {
print("タップされた")
}, label: {
Text("ボタン")
})
Spacer(minLength: 0)
}
}
TextField編
$
のマークがPHP
にしか見えないすね...
私はPHP
ちょっと案件で触ってたんですけど苦い思い出が蘇りました笑
呼び出し方
let testButton: UIButton = {
let button = UIButton()
button.setTitle("ボタン", for: .normal)
button.backgroundColor = .red
button.layer.cornerRadius = 24
return button
}()
let textField: UITextField = {
let input = UITextField()
input.placeholder = "プレースホルダー"
return input
}()
@objc func tapped(_ sender : Any) {
print("入力欄の中身", textField.text)
}
@State var input = ""
var body: some View {
VStack{
Button(action: {
print("入力欄の中身", input)
}, label: {
Text("ボタン")
})
.background(Color.red)
.cornerRadius(24)
TextField("プレースホルダー", text: $input)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
入力欄のパスワード形式
let textField: UITextField = {
let input = UITextField()
input.placeholder = "パスワードを入力してください"
input.isSecureTextEntry = true
return input
}()
@State var pass = ""
var body: some View {
SecureField("パスワードを入力してください", text: $pass)
}
Form編
私の中で知る限りだとこれはSwiftUI
での新しい要素なのかな?と思います。
下記のような枠で囲むやつの実装です。
@State var name = ""
@State var pass = ""
var body: some View {
Text("タイトル").font(.title)
Form{
Text("入力内容\n名前:\(name)\nパスワード\(pass)")
.lineLimit(nil)
TextField("名前を入力してください", text: $name)
SecureField("パスワードを入力してください", text: $pass)
}
Spacer(minLength: 0)
}
##Section編
これは入力できるTableViewとかでみたことがあるUIですね。
SwiftUI
だとこんなに簡単にできちゃいます。
header
があってここに書くとUITableView
でいうセクション
みたいにできるというわけですね
struct ContentView: View {
@State var name = ""
@State var pass = ""
@State var address = ""
@State var tel = ""
var body: some View {
VStack{
Text("Section編").font(.title)
Form{
Section(header: Text("ログイン情報")){
TextField("名前を入力してください", text: $name)
SecureField("パスワードを入力してください", text: $pass)
}
Section(header: Text("パーソナル情報")){
TextField("住所", text: $address)
SecureField("電話番号", text: $tel)
}
}
Spacer(minLength: 0)
}
}
}
Table編
これは一番簡単にできると感動した物でした。
List
やForm
は自身の中にスクロール機能が組み込まれています。
呼び出し
//割と抜粋して書いてます。
class TestViewController: UITabBarDelegate, UITableViewDataSource {
let data: [String] = ["huga", "hoge", "hugahuga"]
let tableView: UITableView = {
let table = UITableView(frame: .zero, style: .grouped)
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return table
}()
tableView.delegate = self
tableView.dataSource = self
self.view.addSubview(tableView)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
?? UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = self.data[indexPath.row]
return cell
}
}
struct ContentView: View {
let data: [String] = ["huga", "hoge", "hugahuga"]
var body: some View {
VStack{
Text("Table編").font(.title)
Form{
Section(header: Text("Test")){
List(data, id:\.self){
Text($0)
}
}
}
Spacer(minLength: 0)
}
}
}
カスタムセル
カスタムセルってSwiftUI
ではないんですかね?
UIKit
だとカスタムセルって言われてた物も割と楽にできちゃいます。
びっくりするくらいコードがスッキリですね.....(恐ろしいSwiftUI)
Identifiableプロトコルについて
class CustomUITableViewCell: UITableViewCell {
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.layer.borderWidth = 1
stack.layer.borderColor = UIColor.red.cgColor
return stack
}()
let name: UILabel = UILabel()
let spacer: UIView = {
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 1)
view.backgroundColor = .gray
return view
}()
let age: UILabel = UILabel()
// 色々抜粋 ...
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier )
addSubview(stackView)
[name, spacer, age].forEach{ stackView.addArrangedSubview($0) }
// AutoLayout抜粋 ...
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setCell(name: String, age: String) {
self.name.text = name
self.age.text = age
}
}
struct CustomCell {
let name: String
let age: String
}
class TestViewController: UITabBarDelegate, UITableViewDataSource {
let data: [CustomCell] = [
CustomCell(name: "山田", age: "24"),
CustomCell(name: "小島", age: "83"),
CustomCell(name: "岡部", age: "39")
]
tableView.delegate = self
tableView.dataSource = self
self.view.addSubview(tableView)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CustomUITableViewCell
cell.setCell(
name: data[indexPath.row].name,
age: data[indexPath.row].age
)
return cell
}
}
struct CustomCell: View, Identifiable {
var id = UUID()
let name: String
let age: String
var body: some View{
VStack{
Text(name)
.bold()
.font(.headline)
Divider()
Text(age)
}.border(Color.red)
}
}
struct ContentView: View {
let data: [CustomCell] = [
CustomCell(name: "山田", age: "24"),
CustomCell(name: "小島", age: "83"),
CustomCell(name: "岡部", age: "39")
]
var body: some View {
VStack{
Text("Table編").font(.title)
Form{
Section(header: Text("Test")){
List(data){ $0 }
}
}
Spacer(minLength: 0)
}
}
}
セルのタップ処理
//割と抜粋して書いてます。
class TestViewController: UITabBarDelegate, UITableViewDataSource {
let data: [String] = ["huga", "hoge", "hugahuga"]
let tableView: UITableView = {
let table = UITableView(frame: .zero, style: .grouped)
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return table
}()
tableView.delegate = self
tableView.dataSource = self
self.view.addSubview(tableView)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
?? UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = self.data[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("\(data[indexPath.row])が選択されました。")
}
}
struct ContentView: View {
let data: [String] = [
"huga", "hoge", "hugahuga"
]
var body: some View {
VStack{
Text("Tableタップ編").font(.title)
Form{
Section(header: Text("Test")){
List(data, id:\.self){ element in
Text(element)
.onTapGesture {
print(element,"が選択されました。")
}
}
}
}
Spacer(minLength: 0)
}
}
}
スクロールできるView
UIKit
のUIScrollView
はAutoLayout
組むのが割とコードで書くの面倒ですよね。
SwiftUI
で楽にできます。
呼び出し
let scroll = UIScrollView()
let outline = UIView()
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
return stack
}()
let labels: [UILabel] = {
let data = ["Test1", "Test2", "Test3", "Test4", "Test5", "Test6", "Test7", "Test8", "Test9", "Test10"]
return data.map {
let label = UILabel()
label.text = $0
return label
}
}()
let spacer: UIView = {
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 1)
view.backgroundColor = .gray
return view
}()
addSubview(scroll)
scroll.addSubview(outline)
outline.addSubview(stackView)
labels.forEach{
stackView.addArrangedSubview($0)
stackView.addArrangedSubview(spacer)
}
struct ContentView: View {
let data = ["Test1", "Test2", "Test3", "Test4", "Test5", "Test6", "Test7", "Test8", "Test9", "Test10"]
var body: some View {
VStack{
Text("スクロール編").font(.title)
ScrollView{
ForEach(data, id: \.self){
Text($0)
.font(.title)
.padding(40)
Divider()
}
}
Spacer(minLength: 0)
}
}
}
Navgation
UIKit
ではUINavgationController
を使ってViewControllerに当てはめてましたが
今回からViewに直接埋め込んで使うことになりました。
UINavigationController(rootViewController: TestViewController())
var body: some View {
NavigationView{
Text("ほげ")
}
}
ナビゲーションのタイトル
self.title = "ナビゲーション"
var body: some View {
NavigationView{
Text("ほげ")
.navigationBarTitle(Text("ナビゲーション"))
}
}
画面遷移
// めちゃくちゃ抜粋しました。
// ボタンを押下してpresentする想定
present(DetailViewController.init(), animated: true, completion: nil)
struct ContentView: View {
var body: some View {
NavigationView{
// 画面遷移
NavigationLink("画面遷移", destination: DetailView(item: "画面遷移した"))
.navigationBarTitle("選択画面")
}
}
}
struct DetailView: View {
let item: String
var body: some View {
Text(item)
}
}
アラート編
デフォルトアラート
// ボタンを押下したら発火する定
let alert = UIAlertController(title: "表示させたいタイトル", message: "表示させたいサブタイトル", preferredStyle: .alert)
let confirmAction = UIAlertAction(title: "確定", style: .default, handler:{
(action: UIAlertAction!) -> Void in
print("確定")
})
let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler:{
(action: UIAlertAction!) -> Void in
print("キャンセル")
})
[cancelAction, confirmAction].forEach{ alert.addAction($0) }
present(alert, animated: true, completion: nil)
struct ContentView: View {
@State private var isShow = false
var body: some View {
NavigationView{
Button("アラートの表示") {
self.isShow = true
}
.alert(isPresented: $isShow) {
Alert(title: Text("表示させたいタイトル"),
message: Text("表示させたいサブタイトル"),
primaryButton: .default(Text("確定"),action: {
print("確定")
}),
secondaryButton: .cancel(Text("キャンセル"),action: {
print("キャンセル")
})
)
}
.navigationBarTitle("選択画面")
}
}
}
アクションシート
let actionSheet = UIAlertController(title: "Menu", message: "", preferredStyle: .actionSheet)
let action1 = UIAlertAction(title: "表示させたいタイトル1", style: .default, handler: {
(action: UIAlertAction!) in
print("表示させたいタイトル1の処理")
})
let action2 = UIAlertAction(title: "表示させたいタイトル2", style: .default, handler: {
(action: UIAlertAction!) in
print("表示させたいタイトル2の処理")
})
let close = UIAlertAction(title: "閉じる", style: .destructive, handler: {
(action: UIAlertAction!) in
//実際の処理
print("閉じる")
})
[action1,action2,close].forEach{ actionSheet.addAction($0) }
self.present(actionSheet,animated: true, completion: nil)
struct ContentView: View {
@State private var isShow = false
var body: some View {
NavigationView{
Button("アラートの表示") {
self.isShow = true
}
.actionSheet(isPresented: $isShow) {
ActionSheet(title: Text("Menu"), message: Text("メッセージ"), buttons:
[
.default(Text("表示させたいタイトル1")) { print("選択肢1") },
.default(Text("表示させたいタイトル2")) { print("選択肢2") },
.cancel(Text("閉じる"), action: { print("閉じる") })
]
)
}
.navigationBarTitle("選択画面")
}
}
}
modalの表示
struct ContentView: View {
@State private var isShow = false
var body: some View {
NavigationView{
VStack{
Button("モーダルViewを表示"){
isShow = true
}.sheet(isPresented: $isShow) {
DetailView()
}
}.navigationBarTitle("Modal画面遷移元")
}
}
}
全体のmodalの表示
let vc = UITestViewController()
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)
struct ContentView: View {
@State private var isShow = false
var body: some View {
NavigationView{
VStack{
Button("モーダルViewを表示"){
isShow = true
}.fullScreenCover(isPresented: $isShow) {
DetailView()
}
}.navigationBarTitle("Modal画面遷移元")
}
}
}
終わりに
まだまだSwiftUI
とUIKit
の違いはあると思いますが、ひとまずは一旦これで終了したいと思います。
ある程度よく使う物たちは共有できたのかなと思います。
UIKit
のコードはぱぱっと作ったコードで動確していないのでもしかしたらおかしい動作するかもしれません..
UIKit
のところについては雰囲気だけお楽しみください.
そこはご了承よろしくお願いいたします。
UIKit
だとあんなに長かったのにってのが学習してる最中終始頭の中でその言葉が流れていました。
本当に便利になったと思います。
これからのSwiftUIライフを楽しみたいと思います!!!
ここまで読んでくださりありがとうございます。
皆さんのお役に少しでも立てましたら幸いです。