- 投稿日:2021-03-06T18:57:52+09:00
【初心者】Swift UIを勉強する その② ーーーSymbolsとList
はじめに
今回のはシンボルとリストの作成方法を学習していきます。
目次
SF Symbols
・appleさんはシンボルアプリを提供していますので、SF Symbolsをダウンロードしましょう。
・Swift UIファイルを新規作成し、SF Symbolsを使ってみましょう。
SF Symbolsアプリにてシンボルの名称をImageとして作成します。
また、デフォルトのシンボルはかなり小さいので、imageScale()
を使って大きさを決めましょう。
だたし、こうやるとシンボルのみサイズが変わるために、シンボルとセットになっているパーツを揃ってデザインを変えるのがおすすめです。CourseRow.swiftstruct CourseRow: View { var body: some View { HStack(alignment: .top) { Image(systemName: "paperplane.circle.fill") .renderingMode(.template) .frame(width: 48.0, height: 48.0) .imageScale(.medium) .foregroundColor(.white) } } } }・シンボルのフレームを48x48に、背景色と
clipShape(Circle())
で円形に切り取りで完成です。
CourseRow.swiftstruct CourseRow: View { var body: some View { HStack(alignment: .top) { Image(systemName: "paperplane.circle.fill") .renderingMode(.template) .frame(width: 48.0, height: 48.0) .imageScale(.medium) .background() .clipShape(Circle()) .foregroundColor(.white) VStack(alignment: .leading, spacing: 4.0) { Text("SwiftUI") .font(.subheadline) .bold() Text("Description") .font(.subheadline) .bold() } Spacer() } }リスト作成
・新規Swift UIファイルを作成し、先ほど作った
CourseRow()
を呼び出します。
・List{}
に詰め込めば完成しました。(UIKitのようにscrollViewがなくでもスクロールできて感動しました)
・Listのスタイルは何かあるのかをみたい時に、
option
を押しながらlistStyle
のドキュメントからチェックできます。
まとめ
・UIKitの半分のコードでlistを作れました。再びSwift UIの強さを感じました。
ソースコードはGithub
参考文献
- 投稿日:2021-03-06T17:17:52+09:00
MVVMとCoordinatorで画面遷移
概要
M(Model)-V(View)-VM(ViewModel)って画面遷移はViewの仕事なのかなと思って調べたところCoordinatorパターンというものがあることをしったので、MVVMにどうにかして組み込んで見ようとしたらできた(なんとか)ので記事にします。
説明
ますどういった仕組みで遷移させるか説明します。
ViewModel
をViewController
のプライベートなプロパティにします。
そしてViewModelBuilder
というViewModel
を初期化するための変数を作ります。
そしてCoordinator
から画面遷移する時にCoordinator.swiftlet vc = ViewController() vc.viewModelBuilder = { let viewModel = ViewModel(input: $0) return viewModel }という感じでコードを書きます。
ここでViewModel
にボタンタップを検知するようにしておけば遷移はできますよね。Coordinator.swiftviewModel.input.buttonTap.subscribe(onNext: { // 遷移処理 }).disposed(by: disposeBag)こんな感じです。
ここで重要なのが今回は単純な画面遷移機能
というところです。
もしこれがログイン画面のCoordinator
だとしたらログイン処理はここですべきではないですよね。
ログイン処理はViewModelでして、画面遷移はcoordinator
に任せたいです。
それはどうやって実現するのかというと、ViewModelvar tapEventSubject = PublishSubject<()>() input.buttonTapped.drive(onNext: { // クロージャを引数にもつログイン処理 { tapEventSubject.onNext(()) } }).disposed(by: disposeBag)このようにログイン処理完了した後にクロージャで
onNext
を流し、coordinatorでCoordinator.swiftlet vc = ViewController() vc.viewModelBuilder = { let viewModel = ViewModel(input: $0) viewModel.tapEventSubject.subscribe(onNext: { // 遷移処理 }).disposed(by: disposeBag) return viewModel }というように参照すれば役割は果たせてるのかなと思います。
まとめ
今回はそれぞれの役割というものにだけ着目していたので
subjectはprivateにしとけよとか、もっとこうすればsubject使わなくてもいけるといった意見があるとございます。改善案、質問などございましたら是非コメントしていただけると嬉しいです。
Coordinatorの利用が今回初めてだったので、もっと勉強して理解を深めて行きたいです。
- 投稿日:2021-03-06T16:24:01+09:00
SwiftUI 枠内どこでも反応、Long Pressも反応するボタンを作成する
やりたい事
①枠内どこでも反応
②Tap:Tap用のアクション
③Long Press:Long Press用のアクション1stアクション
コード
VStack(spacing: 16.0) { Button(action: { print("Button Action!!") }, label: { Text("Button") .frame(width: 200, height: 100, alignment: .center) .overlay(RoundedRectangle(cornerRadius: 16.0).stroke()) }) .onLongPressGesture { print("Button Action Long!!") } Text("Text Button") .frame(width: 200, height: 100, alignment: .center) .overlay(RoundedRectangle(cornerRadius: 16.0).stroke()) .onTapGesture { print("Text Action!!") } .onLongPressGesture { print("Text Action Long!!") } .foregroundColor(.blue) } .padding(32.0) .background(Color(.systemGray6))結果
やりたい事全てを叶える事は出来ない。
ButtonはLong Pressを反応させられない。
Textは枠内どこでも反応させられない。現時点の解決策
コード
VStack(spacing: 16.0) { Text("Text2 Button") .frame(width: 200, height: 100, alignment: .center) .overlay(RoundedRectangle(cornerRadius: 16.0).stroke()) .overlay(RoundedRectangle(cornerRadius: 16.0).foregroundColor(Color.red.opacity(0.0000001))) .onTapGesture { print("Text2 Action!!") } .onLongPressGesture { print("Text2 Action Long!!") } .foregroundColor(.blue) } .padding(32.0) .background(Color(.systemGray6))見た目
試行は続く
・タップとロングプレスをさせるUIデザインがどうなのか?
・他に方法があるのではないか。
- 投稿日:2021-03-06T15:49:03+09:00
Xcodeエラー "this class is not key value coding-compliant for the key"の対処
- 投稿日:2021-03-06T15:15:29+09:00
TextFieldから数字の場合だけ取り出してBMIを計算する方法-処理の切り分け、struct-
TextField
から数字の場合だけを取り出して処理を行いたい時があると思います。
BMIと適正体重の計算を例に、その処理の書き方をいくつかまとめてみました。目次
- 前提確認
- TextFieldから数字を取り出してみる
- 処理を切り出してみる
- 全体のコード
- まとめ
前提確認
- MacOS Catalina 10.15.4
- Xcode 12.1
- Swift version 5
今回計算するBMIと適正体重の求め方は次の通りです。
計算結果は小数点第3位で四捨五入し、Double型で表示することにします。求め方BMI = 体重kg * (身長m)^2 適正体重 = (身長m)^2/22TextFieldから数字を取り出してみる
TextField
の値(=TextField.text
)を数字として取り出すには3つのステップがあります。①
TextField.text
がnil
かどうかチェック、nil
なら空文字を返す
②①の値をDouble型で取り出す
③②のnilチェック(もしTextField.text
が空文字もしくは文字が入っていた場合、②はnilと
なるため)こうして取り出した値は初めて数字として計算することができます。
では、③をguard
を使いつつ、コードを書いてみましょう。ViewControllerimport UIKit class ViewController: UIViewController { @IBOutlet weak private var textField1: UITextField! //身長cmを記入 @IBOutlet weak private var textField2: UITextField! //体重kgを記入 @IBOutlet weak private var bmiOutputLabel: UILabel! //bmiを出力するLabel @IBOutlet weak private var standardWeightOutputLabel: UILabel! //適正体重を出力するLabel @IBAction func calculate(_ sender: Any) { let height = Double(textField1.text ?? "") //①textFeild.textがnilかチェックし、②Double型として身長を取り出す let weight = Double(textField2.text ?? "") //①textFeild.textがnilかチェックし、②Double型として体重を取り出す var bmi = Double() var standardWeight = Double() //③nilチェック、アンラップする guard let _height = height, let _weight = weight else { return //nilならここで処理を止める } //身長をcmからmに直す let meterHegiht = _height/100 //それぞれ小数点第3位で四捨五入する bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100 standardWeight = round(((meterHegiht*meterHegiht)*22)*100)/100 //Labelに表示 bmiOutputLabel.text = "\(bmi)" standardWeightOutputLabel.text = "\(standardWeight)" } }
TextField
から数字を取り出す処理としては、こんな感じです。慣れないうちは面倒だなと思うかもしれません。
もちろんこれでも良いのですが、あとあと保守しやすいよう、この計算を更に別のクラスに切り出してみましょう。処理を切り出してみる
別クラスにこの処理を書いてみましょう。
イメージとして次のように書いてみたいです。イメージimport UIKit class ViewController: UIViewController { @IBOutlet weak private var textField1: UITextField! @IBOutlet weak private var textField2: UITextField! @IBOutlet weak private var bmiOutputLabel: UILabel! @IBOutlet weak private var standardWeightOutputLabel: UILabel! private var calculate = Calculate() @IBAction func calculate(_ sender: Any) { calulate.calculate(inputHeight:textField1.text, inputWeight:textField2.text) } //計算の処理を別クラスに切り分ける class Calculate{ func calculate(inputHeight:String?, inputWeight:String?){ let height = Double(input.height ?? "") let weight = Double(input.weight ?? "") guard let _height = height, let _weight = weight else { return } let meterHegiht = _height/100 let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100 let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100 } }ただ、これにはいくつか問題が含まれています。
一つ一つ見ていきましょう。①返したい値が2つ
Calulate
クラスのcalculate
メソッドで返したい値はBMI
と適正体重
の2つです。
2つのDouble型を返のにはどうすれば良いでしょうか。Calculateクラスfunc calculate(inputHeight:String?, inputWeight:String?) -> //2つのDouble型?そこで2つ以上の値を管理するのに便利な
struct
が使えます。struct Output { let bmi : String let standardWeight :String }今回、出力する値も2つなら、入力する値も2つ(身長と体重)なので、こちらも
struct
を使ってみましょう。
ただ、こちらはTextField
からの値が入り、nil
になる可能性もあるため、オプショナル型とします。struct Input { let height : String? let weight : String? }さて、上二つを使うと次のようにまとめられそうです。
import UIKit class ViewController: UIViewController { (略) } struct Input { let height : String? let weight : String? } struct Output { let bmi : String let standardWeight :String } class Calculate{ func calculate(input: Input) -> Output{ let height = Double(input.height ?? "") let weight = Double(input.weight ?? "") guard let _height = height, let _weight = weight else { return } let meterHegiht = _height/100 let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100 let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100 return Output(bmi: "\(bmi)", standardWeight:"\( standardHeight)") } }
struct
を使用して入力値と出力値をまとめられて、スッキリしましたね。
ただ、実はこれだとguard
の中身でエラーが発生します。
というのも、返り値としてOutput
を設定しているのに、return
だけだと何も返さないので怒られます。
次のようにすることもできますが、せっかくならもう少し汎用性のある方法で書いてみましょう。guard let _height = height, let _weight = weight else { //heightまたはweightがnilであれば次の値を返す return Output(bmi: "誤入力", standardWeight: "誤入力") //今回たまたまOutputの中身がStringなのでこういう書き方ができる }②ErrorTypeを定義
ということで、
guard
の条件をクリアしなかった場合はそれをError
として補足しましょう。
例のErrorType
プロトコルを適合させたenum
でエラーパターンを定義します。この辺理解曖昧な方はこちらの記事をご参照。enum Validation: Error { case 正しく入力されていない(result: String) }
guard
をクリアしなかった場合、このエラーバターンをthrow
することにします。struct Input { let height : String? let weight : String? } struct Output { let bmi : String let standardWeight :String } class Calculate{ enum Validation: Error { case 正しく入力されていない(result: String) } func calculate(input: Input) throws -> Output{ let height = Double(input.height ?? "") let weight = Double(input.weight ?? "") guard let _height = height, let _weight = weight else { throw Validation.正しく入力されていない(result: "正しく入力されていません") } let meterHegiht = _height/100 let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100 let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100 return Output(bmi: "\(bmi)", standardWeight:"\( standardHeight)") } }これで切り分けた方の処理を完了です。
最後にViewController
内の処理を書きましょう。③do構文で仕上げ
今回エラーパターンを含む処理を書くので、例の
do構文
を使います。
と言っても非常にシンプルです。ViewControllerimport UIKit class ViewController: UIViewController { @IBOutlet weak private var textField1: UITextField! @IBOutlet weak private var textField2: UITextField! @IBOutlet weak private var bmiOutputLabel: UILabel! @IBOutlet weak private var standardWeightOutputLabel: UILabel! private var calculate = Calculate() @IBAction func calculate(_ sender: Any) { do { let Output = try calculate.calculate(input: Input(height: textField1.text, weight: textField2.text)) bmiOutputLabel.text = "\(Output.bmi)" standardWeightOutputLabel.text = "\(Output.standardWeight)" }catch let Calculate.Validation.正しく入力されていない(result: msg){ print(msg) }catch{ } } }全体のコード
まとめるとこうなります。
ViewControllerimport UIKit class ViewController: UIViewController { @IBOutlet weak private var textField1: UITextField! @IBOutlet weak private var textField2: UITextField! @IBOutlet weak private var bmiOutputLabel: UILabel! @IBOutlet weak private var standardWeightOutputLabel: UILabel! private var calculate = Calculate() @IBAction func calculate(_ sender: Any) { do { let Output = try calculate.calculate(input: Input(height: textField1.text, weight: textField2.text)) bmiOutputLabel.text = "\(Output.bmi)" standardWeightOutputLabel.text = "\(Output.standardWeight)" }catch let Calculate.Validation.正しく入力されていない(result: msg){ print(msg) }catch{ } } } struct Input { let height : String? let weight : String? } struct Output { let bmi : String let standardWeight :String } class Calculate{ enum Validation: Error { case 正しく入力されていない(result: String) } func calculate(input: Input) throws -> Output{ let height = Double(input.height ?? "") let weight = Double(input.weight ?? "") guard let _height = height, let _weight = weight else { throw Validation.正しく入力されていない(result: "正しく入力されていません") } let meterHegiht = _height/100 let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100 let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100 return Output(bmi: "\(bmi)", standardWeight:"\( standardHeight)") } }まとめ
いかがでしたでしょうか。
最初のコードより少し長くなってしまいましたが、処理を切り分けられたの保守しやすくなったのではないでしょうか。
また、struct
で値も一括管理しているので、可読性も良くなったと思います。自分がこの処理を書くまでにで辿った思考を一つ一つ言語していきましたが、何かの参考になれば幸いです。
ご指摘等ございましたら頂けますと幸いです。
- 投稿日:2021-03-06T11:32:00+09:00
Initialization Closureとglobal定数を組み合わせて、computedライクなglobal定数を作る
Initialization Closureとは?
Initialization closureは、以下の特徴を持ちます
- closure内でpropertyに操作を加えられる
- stored propertyである(値が割り当てられるタイミングはclassからinstanceが生成された時)
If a stored property’s default value requires some customization or setup, you can >use a closure or global function to provide a customized default value for that >property. Whenever a new instance of the type that the property belongs to is >initialized, the closure or function is called, and its return value is assigned >as the property’s default value.
主にUIをコードで記述する際に用いられます。
参考記事Globalな定数(変数)の特徴
Globalな定数(変数)は以下の特徴を持つ
- lazyである(割り当てられるタイミングは値が呼ばれた時)
Global constants and variables are always computed lazily, in a similar manner to >Lazy Stored Properties. Unlike lazy stored properties, global constants and >variables don’t need to be marked with the lazy modifier.
Local constants and variables are never computed lazily.それぞれの特徴を組み合わせると、computedライクなglobal定数を作ることができます。
ViewController.swiftlet somePattern: FetchedValue = { let defalutValue = "hoge" return isFetchedCompleted ? fetchedValue : defaultValue }()これは、外部サービスを使って、複数画面でABテストを行いたい/アプリの挙動を変えたい場合に以下の性質を持つので便利です。
- lazyなので、アプリ起動後にfetchが間に合わない場合でも、対象画面に遷移するまで時間を稼げる(=テストパターンに割り振られるユーザー数を増やせる)
- globalなので、各画面でfetchを行わなくて良い
- 各画面でfetchを行わないので、画面毎に挙動が異なることがない
- 投稿日:2021-03-06T11:28:57+09:00
SwiftUIからUIKitへ値を渡す。
はじめに
SwiftUIからUIKitの利用を理解する為の二歩目として
・値を渡す。
・値が変わったら、表示を変更する。
最低限の実装方法を記載したものです。
最初の一歩は下記参照
https://qiita.com/ikaasamay/items/108ac5c211b75a9739d4環境
macOS Big Sur 11.1
XCode 12.4
Swift 5実装
SwiftUI側でボタン押下時に
countを+1して、
UIKit側では+1された値を追従して表示しています。ContentView.swiftimport SwiftUI struct ContentView: View { @State var count = 0 var body: some View { ZStack (alignment: .center) { // SwiftUI ------------- VStack { HStack { Text("SwiftUI") Spacer() } // 加算ボタン Button(action: { // 加算処理 count += 1 }){ // 加算ボタンのレイアウト Text("加算ボタン") .font(.largeTitle) // ボタンらしくレイアウトを調整 .frame(width: 250, height: 40, alignment: .center) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.blue, lineWidth: 2) ) } Spacer() } // UIKitへcountを引数で渡す。 CountUIKitLabel(count: $count) .frame(height: 100) } } } struct CountUIKitLabel:UIViewRepresentable { // 表示する値を保持するプロパティ @Binding var count:Int // SwiftUI側に返却するUILabelのインスタンスを返却する。 func makeUIView(context: Context) -> UILabel { let label = UILabel() label.backgroundColor = UIColor.red label.textAlignment = NSTextAlignment.center return label } // プロパティ(count)が変更されると、その都度実行する。 func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = "UIKit " + count.description } } 次回はコーディネーターを記事にする予定
- 投稿日:2021-03-06T10:41:35+09:00
UITableViewの高さをcell数に応じて可変にしたい
課題事例
- 元々、画面全体がUIScrollViewになっていて、真ん中にUITableViewを差し込みたい(※新規画面なら全体をUITableViewにすれば済む話ではあります)。
- 画面全体をスクロールさせる点に変わりはないので、UITableViewはスクロールさせない。
- UITableViewはcell数が可変、かつcellごとの高さも違っていて、それに応じてUITableViewの高さを変えたい(そうしないと全てのcellが表示されなかったり、余白ができてしまったりするので)。
解決法
(1) 高さのconstraintを変数に格納。
private weak var tableViewHeightConstraint: NSLayoutConstraint?(2) KVO(Key Value Observing)でUITableViewのcontentSize.heightを監視。
NSKeyValueObservationを格納するための変数を宣言し、UITableViewのcontentSize.heightをobserve。
heightが変わった時に上の制約のconstantをセットする処理を定義する。private var observation: NSKeyValueObservation? override func viewDidLoad() { // 略 observation = tableView.observe(\.contentSize, options: [.new]) { [weak self] (_, _) in guard let self = self else { return } self.tableViewHeightConstraint?.constant = self.tableView.contentSize.height } }(3) 後始末
deinit { observation?.invalidate() observation = nil }私は以上のような解決法で課題をクリアできましたが、もっとシンプルな解決法をご存知でしたら、ぜひ教えてください。
- 投稿日:2021-03-06T08:45:12+09:00
[iOS] ダークモードを考慮したAsset Catalogの管理
本記事では、Swiftの画像の命名規則とAsset Catalogのフォルダ構成について、個人で最適だと考える方法について説明します。
画像の命名規則について
注意点
本記事では,画像のパターンが2種類あります。
例)sampleという画像の名前の場合
Asset Catalog内の画像
→ sampleAsset Catalogに追加する前の画像
→ sample.png命名規則
画像の命名は以下で統一
スネークケースを使用
ic_画像名_修飾.png
- 小さめなアイコンの画像
- 例:
ic_star_fill.png
img_画像名_修飾.png
- 大きめなillust, logoなどの画像
- 例:
img_logo.png
修飾について以下の例があります。
- fill
- off/onダークモード用の画像の場合
語尾にdarkを付ける
-ic_画像名_修飾_dark.png
-img_画像名_修飾_dark.png
ダークモード用の画像分けて命名する理由
iOSでは,同じ命名で各モードの画像をAsset Catalog内に追加すると,〇〇-1.pngのように重複によって番号が付与されてしまう.
内部的に不都合が生じるため,ダークモード用の画像の命名が必要であるAsset Catalogのフォルダ
現在のAsset Catalog内のフォルダ構成は以下の通りです。
Assets
- iconフォルダ
ic_画像名
- imageフォルダ
img_画像名
- colorフォルダ