20210306のSwiftに関する記事は9件です。

【初心者】Swift UIを勉強する その② ーーーSymbolsとList

はじめに

今回のはシンボルとリストの作成方法を学習していきます。

目次

  1. SF Symbols
  2. リスト作成
  3. まとめ
  4. 参考文献

SF Symbols

・appleさんはシンボルアプリを提供していますので、SF Symbolsをダウンロードしましょう。

截屏2021-02-23 15.19.40.png
・Swift UIファイルを新規作成し、SF Symbolsを使ってみましょう。
 SF Symbolsアプリにてシンボルの名称をImageとして作成します。
 また、デフォルトのシンボルはかなり小さいので、imageScale()を使って大きさを決めましょう。
 だたし、こうやるとシンボルのみサイズが変わるために、シンボルとセットになっているパーツを揃ってデザインを変えるのがおすすめです。

CourseRow.swift
struct 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.swift
struct 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がなくでもスクロールできて感動しました)
截屏2021-02-23 16.54.11.png

・Listのスタイルは何かあるのかをみたい時に、optionを押しながらlistStyleのドキュメントからチェックできます。
截屏2021-02-23 16.57.38.png

まとめ

・UIKitの半分のコードでlistを作れました。再びSwift UIの強さを感じました。

ソースコードはGithub

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVVMとCoordinatorで画面遷移

概要

M(Model)-V(View)-VM(ViewModel)って画面遷移はViewの仕事なのかなと思って調べたところCoordinatorパターンというものがあることをしったので、MVVMにどうにかして組み込んで見ようとしたらできた(なんとか)ので記事にします。

説明

ますどういった仕組みで遷移させるか説明します。
ViewModelViewControllerのプライベートなプロパティにします。
そしてViewModelBuilderというViewModelを初期化するための変数を作ります。
そしてCoordinatorから画面遷移する時に

Coordinator.swift
let vc = ViewController()
vc.viewModelBuilder = {
    let viewModel = ViewModel(input: $0)
    return viewModel
}

という感じでコードを書きます。
ここでViewModelにボタンタップを検知するようにしておけば遷移はできますよね。

Coordinator.swift
viewModel.input.buttonTap.subscribe(onNext: {
    // 遷移処理
}).disposed(by: disposeBag)

こんな感じです。
ここで重要なのが今回は単純な画面遷移機能というところです。
もしこれがログイン画面のCoordinatorだとしたらログイン処理はここですべきではないですよね。
ログイン処理はViewModelでして、画面遷移はcoordinatorに任せたいです。
それはどうやって実現するのかというと、

ViewModel
var tapEventSubject = PublishSubject<()>()

input.buttonTapped.drive(onNext: {
    // クロージャを引数にもつログイン処理 {
        tapEventSubject.onNext(())
    }
}).disposed(by: disposeBag)

このようにログイン処理完了した後にクロージャでonNextを流し、coordinatorで

Coordinator.swift
let vc = ViewController()
vc.viewModelBuilder = {
    let viewModel = ViewModel(input: $0)
    viewModel.tapEventSubject.subscribe(onNext: {
        // 遷移処理
    }).disposed(by: disposeBag)
    return viewModel
}

というように参照すれば役割は果たせてるのかなと思います。

まとめ

今回はそれぞれの役割というものにだけ着目していたので
subjectはprivateにしとけよとか、もっとこうすればsubject使わなくてもいけるといった意見があるとございます。

改善案、質問などございましたら是非コメントしていただけると嬉しいです。
Coordinatorの利用が今回初めてだったので、もっと勉強して理解を深めて行きたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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デザインがどうなのか?
・他に方法があるのではないか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcodeエラー "this class is not key value coding-compliant for the key"の対処

Xcode初学者でよく起こるエラーとのことで、自分もはまってしまったので備忘録です。

エラーが起きる原因

@IBOutlet@IBActionをコードから消したが、StoryBoardから削除していない
・ クラスを同じ名前で作り直したが、以前設定したStoryBoardのTriggered Seguesを削除していない

対処

StoryBoard側も削除して再ビルド

スクリーンショット 2021-03-06 15.45.21.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TextFieldから数字の場合だけ取り出してBMIを計算する方法-処理の切り分け、struct-

TextFieldから数字の場合だけを取り出して処理を行いたい時があると思います。
BMIと適正体重の計算を例に、その処理の書き方をいくつかまとめてみました。

simulator_screenshot_0D5EF4E7-A499-4017-837A-77A787F10E78.png

目次

  • 前提確認
  • TextFieldから数字を取り出してみる
  • 処理を切り出してみる
  • 全体のコード
  • まとめ

前提確認

  • MacOS Catalina 10.15.4
  • Xcode 12.1
  • Swift version 5

今回計算するBMIと適正体重の求め方は次の通りです。
計算結果は小数点第3位で四捨五入し、Double型で表示することにします。

求め方
BMI = 体重kg * (身長m)^2
適正体重 = (身長m)^2/22

TextFieldから数字を取り出してみる

TextFieldの値(=TextField.text)を数字として取り出すには3つのステップがあります。

TextField.textnilかどうかチェック、nilなら空文字を返す
②①の値をDouble型で取り出す
③②のnilチェック(もしTextField.textが空文字もしくは文字が入っていた場合、②はnilとなるため)

こうして取り出した値は初めて数字として計算することができます。
では、③をguardを使いつつ、コードを書いてみましょう。

ViewController
import 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構文を使います。
と言っても非常にシンプルです。

ViewController
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) {

        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{
        }
    }
}

全体のコード

まとめるとこうなります。

ViewController
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) {

        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で値も一括管理しているので、可読性も良くなったと思います。

自分がこの処理を書くまでにで辿った思考を一つ一つ言語していきましたが、何かの参考になれば幸いです。
ご指摘等ございましたら頂けますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.swift
let somePattern: FetchedValue = {
   let defalutValue = "hoge"
   return isFetchedCompleted ? fetchedValue : defaultValue
}()

これは、外部サービスを使って、複数画面でABテストを行いたい/アプリの挙動を変えたい場合に以下の性質を持つので便利です。

  • lazyなので、アプリ起動後にfetchが間に合わない場合でも、対象画面に遷移するまで時間を稼げる(=テストパターンに割り振られるユーザー数を増やせる)
  • globalなので、各画面でfetchを行わなくて良い
  • 各画面でfetchを行わないので、画面毎に挙動が異なることがない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIからUIKitへ値を渡す。

はじめに

SwiftUIからUIKitの利用を理解する為の二歩目として
・値を渡す。
・値が変わったら、表示を変更する。
最低限の実装方法を記載したものです。
最初の一歩は下記参照
https://qiita.com/ikaasamay/items/108ac5c211b75a9739d4

環境

macOS Big Sur 11.1
XCode 12.4
Swift 5

実装

加算ボタンを押したら、値が+1されます。
スクリーンショット 2021-03-06 11.14.43.png

SwiftUI側でボタン押下時に
countを+1して、
UIKit側では+1された値を追従して表示しています。

ContentView.swift
import 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
    }
}

次回はコーディネーターを記事にする予定

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
}

私は以上のような解決法で課題をクリアできましたが、もっとシンプルな解決法をご存知でしたら、ぜひ教えてください。:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS] ダークモードを考慮したAsset Catalogの管理

本記事では、Swiftの画像の命名規則とAsset Catalogのフォルダ構成について、個人で最適だと考える方法について説明します。

画像の命名規則について

注意点

本記事では,画像のパターンが2種類あります。
例)sampleという画像の名前の場合
Asset Catalog内の画像
→ sample

Asset 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フォルダ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む