20201024のSwiftに関する記事は14件です。

[Swift] TableViewの備忘録

今回の題

todoリストを作る過程でTableViewを使ったので覚えている内にどこかに書き残しておこうという魂胆で書いてます。
特に物珍しいことはしていません。

シングルビューアプリ上のtableViewでセルの

  • 表示
  • 並び替え
  • 削除

を行うくらいの内容です。

諸々のバージョン

  • Swift version 5.3
  • Xcode Version 11.1

メインストーリーボード

以下の状態を作ります。

スクリーンショット 2020-10-24 18.07.52.png

流れは、

  1. デフォルトのViewControllerにNavigationControllerを追加
  2. ViewController上にtableViewを配置
  3. 2のtableView上にtableViewCellを配置
  4. tableViewCellのidentifierをcellに設定

です。

続いて、tableViewのdelegateとdataSourceもメインストーリーボードで設定しておきます。

スクリーンショット 2020-10-24 18.33.36.png

これをしておけば、viewControllerで

tableView.delegate = self
tableView.datasource = self

とか書かずに済みます。
ただ、ここら辺の書き方は現場に合わせるのが一番かと思いますが、どちらが主流なんでしょうね?
Swift歴独学数週間の私にはわかりませんが、もしご存知の方がいらっしゃいましたらお教えいただけますと幸いです。

TableViewに配列を表示する

スクリーンショット 2020-10-24 19.53.49.png

viewController

以下のように、tableViewをOutlet接続し、配列を定義します。

ViewController.swift
class ViewController: UIViewController {

    // 以下2行を追記
    private var todos = ["卵を買う", "牛乳を買う", "スポンジを買う", "イチゴを買う"]
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}

続いて、セルの数セルに表示する内容を定義します。
これらはviewControllerクラス内に定義してもいいのですが、今回はviewControllerを汚さず、かつ可読性を高めるためにextensionを使ってプロトコルごとに定義していきます。

表示するには、UITableViewDataSourceプロトコルの必須メソッドを2つ使います。
以下をviewControllerクラスの下に書きます。

ViewController.swift
extension ViewController: UITableViewDataSource {

    // セルの数を定義。 ここでは配列の数のセルを指定。
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todos.count
    }

    // セルの各行の内容を定義
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = todos[indexPath.row]
        return cell
    }
}

表示はこれで完成です。

TableViewを操作する

TableViewを操作する際には、主に下の画像にあるメソッドとプロパティを用いて編集モードに切り替えて行います。
スクリーンショット 2020-10-24 20.17.59.png
なのでまず以下でその流れ。

編集モードに切り替える

その1 ボタンを配置

こんな感じになります。
スクリーンショット 2020-10-24 20.31.41.png
まず、navigationBarに編集モードに切り替えるためのボタンを配置します。
ボタンは自前で用意するのではなく、最初に挙げた画像にあるeditButtonItemを使います。
ViewControllerクラスのviewDidLoad()メソッドを以下のように編集します。

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        // 追記 navigationBarの右側に編集モードに切り替えるためのボタンを配置
        navigationItem.rightBarButtonItem = editButtonItem
    }

その2 メソッドを定義

ViewControllerクラスに以下のメソッドを定義します。
このメソッドはその1のボタンが押されると呼ばれるメソッドで、編集モードに移行する内容になっています。
メソッド内の2行目、isEditingはtrueであれで編集を許可、falseであれば不許可とするプロパティです。

ViewController.swift
    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        tableView.isEditing = editing
    }

これでボタンを押すと以下のように編集モードに切り替えられるようになりました。
スクリーンショット 2020-10-24 21.53.02.png

セルを並び替える

ezgif.com-gif-maker.gif
UITableViewDelegateプロトコルのメソッドをextensionを定義して書き出します。
セルを並び替える為に定義するメソッドは以下の二つ。

ViewController.swift
extension ViewController: UITableViewDelegate {

    // セルの移動を許可する
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // セル移動時の処理。 セルの移動前の位置と移動後の位置を入れ替える処理を行う。
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let sourceCellItem = todos[sourceIndexPath.row]
        guard let indexPath = todos.firstIndex(of: sourceCellItem) else { return }
        todos.remove(at: indexPath)
        todos.insert(sourceCellItem, at: destinationIndexPath.row)
    }
}

各メソッドの説明はコメントの通りです。

セルを削除する

ezgif.com-gif-maker (1).gif

先ほどのextensionに追記。
追記定義するメソッドは以下の一つだけ。

ViewController.swift
extension ViewController: UITableViewDelegate {
    // 削除を行うメソッド
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        // 配列から削除
        todos.remove(at: indexPath.row)
        // セルから削除
        tableView.deleteRows(at: [indexPath as IndexPath], with: UITableView.RowAnimation.automatic)
    }
}

ただこれだと編集モード時以外でも、セルを横にスワイプすると削除できてしまいます。
そうしたくない時は以下のメソッドを追記します。

ViewController.swift
    // 編集モード中だけ削除を可能に
    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
        if tableView.isEditing {
            return .delete
        }
        return .none
    }

メモ書き

セルの複数選択を有効にする

今回は使いませんでしたが、調べたのでメモしておきます。
ViewControllerのviewDidLoad()に以下を追記します。

ViewController.swift
// 複数選択を可能にする
tableView.allowsMultipleSelectionDuringEditing = true

ただこれを書くと、編集モード中にスワイプで削除はできなくなります。

選択したcellのパスは、

tableView.indexPathsForSelectedRows

で取得できます。

以上!!!

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

[Swift] UITableViewの備忘録

今回の題

todoリストを作る過程でTableViewを使ったので覚えている内にどこかに書き残しておこうという魂胆で書いてます。
特に物珍しいことはしていません。

シングルビューアプリ上のtableViewでセルの

  • 表示
  • 並び替え
  • 削除

を行うくらいの内容です。

諸々のバージョン

  • Swift version 5.3
  • Xcode Version 11.1

メインストーリーボード

以下の状態を作ります。

スクリーンショット 2020-10-24 18.07.52.png

流れは、

  1. デフォルトのViewControllerにNavigationControllerを追加
  2. ViewController上にtableViewを配置
  3. 2のtableView上にtableViewCellを配置
  4. tableViewCellのidentifierをcellに設定

です。

続いて、tableViewのdelegateとdataSourceもメインストーリーボードで設定しておきます。

スクリーンショット 2020-10-24 18.33.36.png

これをしておけば、viewControllerで

tableView.delegate = self
tableView.datasource = self

とか書かずに済みます。
ただ、ここら辺の書き方は現場に合わせるのが一番かと思いますが、どちらが主流なんでしょうね?
Swift歴独学数週間の私にはわかりませんが、もしご存知の方がいらっしゃいましたらお教えいただけますと幸いです。

TableViewに配列を表示する

スクリーンショット 2020-10-24 19.53.49.png

viewController

以下のように、tableViewをOutlet接続し、配列を定義します。

ViewController.swift
class ViewController: UIViewController {

    // 以下2行を追記
    private var todos = ["卵を買う", "牛乳を買う", "スポンジを買う", "イチゴを買う"]
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}

続いて、セルの数セルに表示する内容を定義します。
これらはviewControllerクラス内に定義してもいいのですが、今回はviewControllerを汚さず、かつ可読性を高めるためにextensionを使ってプロトコルごとに定義していきます。

表示するには、UITableViewDataSourceプロトコルの必須メソッドを2つ使います。
以下をviewControllerクラスの下に書きます。

ViewController.swift
extension ViewController: UITableViewDataSource {

    // セルの数を定義。 ここでは配列の数のセルを指定。
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todos.count
    }

    // セルの各行の内容を定義
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = todos[indexPath.row]
        return cell
    }
}

表示はこれで完成です。

TableViewを操作する

TableViewを操作する際には、主に下の画像にあるメソッドとプロパティを用いて編集モードに切り替えて行います。
スクリーンショット 2020-10-24 20.17.59.png
なのでまず以下でその流れ。

編集モードに切り替える

その1 ボタンを配置

こんな感じになります。
スクリーンショット 2020-10-24 20.31.41.png
まず、navigationBarに編集モードに切り替えるためのボタンを配置します。
ボタンは自前で用意するのではなく、最初に挙げた画像にあるeditButtonItemを使います。
ViewControllerクラスのviewDidLoad()メソッドを以下のように編集します。

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        // 追記 navigationBarの右側に編集モードに切り替えるためのボタンを配置
        navigationItem.rightBarButtonItem = editButtonItem
    }

その2 メソッドを定義

ViewControllerクラスに以下のメソッドを定義します。
このメソッドはその1のボタンが押されると呼ばれるメソッドで、編集モードに移行する内容になっています。
メソッド内の2行目、isEditingはtrueであれで編集を許可、falseであれば不許可とするプロパティです。

ViewController.swift
    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        tableView.isEditing = editing
    }

これでボタンを押すと以下のように編集モードに切り替えられるようになりました。
スクリーンショット 2020-10-24 21.53.02.png

セルを並び替える

ezgif.com-gif-maker.gif
UITableViewDelegateプロトコルのメソッドをextensionを定義して書き出します。
セルを並び替える為に定義するメソッドは以下の二つ。

ViewController.swift
extension ViewController: UITableViewDelegate {

    // セルの移動を許可する
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // セル移動時の処理。 セルの移動前の位置と移動後の位置を入れ替える処理を行う。
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let sourceCellItem = todos[sourceIndexPath.row]
        guard let indexPath = todos.firstIndex(of: sourceCellItem) else { return }
        todos.remove(at: indexPath)
        todos.insert(sourceCellItem, at: destinationIndexPath.row)
    }
}

各メソッドの説明はコメントの通りです。

セルを削除する

ezgif.com-gif-maker (1).gif

先ほどのextensionに追記。
追記定義するメソッドは以下の一つだけ。

ViewController.swift
extension ViewController: UITableViewDelegate {
    // 削除を行うメソッド
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        // 配列から削除
        todos.remove(at: indexPath.row)
        // セルから削除
        tableView.deleteRows(at: [indexPath as IndexPath], with: UITableView.RowAnimation.automatic)
    }
}

ただこれだと編集モード時以外でも、セルを横にスワイプすると削除できてしまいます。
そうしたくない時は以下のメソッドを追記します。

ViewController.swift
    // 編集モード中だけ削除を可能に
    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
        if tableView.isEditing {
            return .delete
        }
        return .none
    }

メモ書き

セルの複数選択を有効にする

今回は使いませんでしたが、調べたのでメモしておきます。
ViewControllerのviewDidLoad()に以下を追記します。

ViewController.swift
// 複数選択を可能にする
tableView.allowsMultipleSelectionDuringEditing = true

ただこれを書くと、編集モード中にスワイプで削除はできなくなります。

選択したcellのパスは、

tableView.indexPathsForSelectedRows

で取得できます。

以上!!!

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

Nib(.xib)ファイルからViewControllerを初期化

簡単に呼べます。
NibViewController.swift
NibViewController.xib
をつくったとします。

let nvc = NibViewController.init(nibName: "NibViewController", bundle: nil)
self.navigationController?.show(nvc, sender: nil)

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

【Swift】NavigationBarの色が指定した色と違う(薄くなる)

ナビゲーションバーの色をnavigationController?.navigationBar.barTintColorで設定した際に、設定した色と違う色になって困りました。

問題

完成イメージ

ナビゲーションの色をメインの背景の色と同様にデザインしました。

しかしnavigationController?.navigationBar.barTintColorにメインの背景と同じ色を設定したところ、実際に表示されたナビゲーションは少し薄い色になってしまっていました。

問題のイメージ

解決

NavigationBar.isTranslucentが原因でした。
NavigationBar.isTranslucentはナビゲーションバーの透明度を指定する値です。ナビゲーションバーに画像を設定していない場合はデフォルトはtrueが設定されており半透明になっています。半透明にしたくない場合はこの値をfalseにすれば解決です。

抜粋

If the navigation bar doesn't have a custom background image, or if any pixel of the background image has an alpha value of less than 1.0, the default value of this property is true. If the background image is completely opaque, the default value of this property is false. If you set this property to true and the custom background image is completely opaque, UIKit applies a system-defined opacity of less than 1.0 to the image. If you set this property to false and the background image is not opaque, UIKit adds an opaque backdrop.

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UINavigationBar.appearance().isTranslucent = false
    return true
}

isTranslucent=falseにするとUISearchControllerが影響をうける

NavigationBar.isTranslucentfalseを設定すると、ナビゲーションバーに設定したUISearchControllerにフォーカスした際に検索バーが下にズレる現象が起きます。
その際は以下の実装をControllerに記載すれば解決します。

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

Firebase Remote Configの導入手順

Firebase Remote Configとは

Firebase Remote Config のコンソール側でKey:Valueの値を登録しておくことで、その値をクラウドで更新することができます。

メリット

アプリの動作や外観を変更する際に、アプリのアップデートを配布せずとも、Firebase Remote Configのコンソール上で設定を修正するだけで変更することができます。
基本的に全ユーザーに対して同じKey-Valueをリアルタイムに配信・反映でき以下のようなケースに利用することができます。

  • 条件(アプリバージョン、OS、指定%のユーザー等)を指定してValueを変える事もできる。
  • A/Bテストにも使用することができる。

よくアプリのバージョンをFirebase Remote Configで管理し、強制アップデートを促す際に使われることが多いそうです。

デメリット

即時反映には向いていない

基本的にフェッチ(クラウドから値の取得)は12時間に1度しか行われません。一度フェッチを行った場合は、キャッシュが残り再度フェッチするまでの間はキャッシュされた値が使用されます。そのため、即時変更を求める値の取得についてはFirebase Remote Configの利用は適していません。

導入手順

Firebaseコンソールでの設定

1. Firebaseコンソールを開き、メニューからRemote Configを選択します。

2. 条件の設定

ここでは、それぞれ値を取得する際の条件を設定することができます。

条件については

  • デバイスの地域 / 国
  • 現在の時刻とデバイスで取得された時刻
  • アプリのバージョン
  • ビルド番号

などで設定できます。

ここでは、デバイスが日本かどうかで判別していきます
image.png

などで条件を設定することも可能です。

3. パラメーターキーの設定

上記で決めた条件に基づいて、

-  デバイスの地域 / 国 = 日本 の場合:ヤフーのURL
-  デバイスの地域 / 国 = 日本以外 の場合:グーグルのURL

を取得するように設定していきます。
これによってデバイスの国によって遷移する先のURLを変更することができます。

Swift側

1.Remote Configオブジェクトの作成

remoteConfig = RemoteConfig.remoteConfig()
let settings = RemoteConfigSettings()

// フェッチ間隔の設定(開発環境のみ設定)
settings.minimumFetchInterval = 0

remoteConfig.configSettings = settings

Remote Configは基本的に一度フェッチしたらキャッシュ時間内(デフォルトでは12時間)では再度フェッチされません。
そのため、開発環境では上記のようにminimumFetchIntervalを設定する必要があります。これにより開発環境ではフェッチ間隔に関係なくフェッチを行うことができます。ただし、このコードは必ず本番環境に書かないように注意してください。

この設定は開発目的でのみ使用し、本番環境で実行されるアプリには使用しないでください。10 人の小さな開発チームでアプリをテストするだけの場合、毎時のサービス側の割り当て制限に達する可能性はほとんどありません。しかし、最小フェッチ間隔に非常に小さい値を設定して何千人ものテストユーザーにアプリを push すると、アプリがこの割り当てに達する可能性が高くなります。

https://firebase.google.com/docs/remote-config/use-config-ios?hl=ja

2.デフォルト値の設定

Firebase Remote Config側に値が設定されていなかったり、オフラインで取得できない際に使うデフォルト値をここで設定します。
plistファイルを作成し、一連のパラメータ名とデフォルト値を定義します。

// RemoteConfigDefaultsはplistファイルのファイル名
remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults")

3.値の読み込み

remoteConfig.fetchAndActivate { (status, error) -> Void in
            if status == .successFetchedFromRemote || status == .successUsingPreFetchedData {
                // fetchが完了した際の処理を記述
                print("Config fetched!")
                // 今回取得した値はjson形式のためデコードする必要があります
                let jsonString = remoteConfig["parameter"].stringValue
                let jsonData = jsonString?.data(using: .utf8)
                do {
                    // getUrlにRemote Configから取得した値を格納している
                    var urlDictionary = try JSONSerialization.jsonObject(with: jsonData!, options: [])
                    guard let dictionary = urlDictionary as? [String: Any],
                    let getUrl = dictionary["url"] as? [String: Any] else { return }
            } else {
                // fetchに失敗した際の処理を記述
                print("Config not fetched")
            }
        }
}

fetchAndActivateによって値のフェッチとfetchした値を有効化する処理を一度に行っています。
このように実装することでFirebase Remote Configから値を取得、アプリ内で利用することができます。

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

【Swift5】UITableView上で、キーボードを閉じる処理

はじめに

TableViewのセルにTextFieldTextViewを配置した際に、これら以外の部分をタップした時のキーボードを閉じる処理に少し手こずりましたので、備忘録として投稿します。

動作環境

【Xcode】Version 12.0.1
【Swift】Version 5.3

実装コード

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.delegate = self
        tableView.dataSource = self

        // タップ認識するためのインスタンスを生成
        let tapGesture = UITapGestureRecognizer(
            target: self,
            action: #selector(dismissKeyboard))
        tapGesture.cancelsTouchesInView = false
        // Viewに追加
        view.addGestureRecognizer(tapGesture)
    }
    // キーボードと閉じる際の処理
    @objc public func dismissKeyboard() {
        view.endEditing(true)
    }

}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath as IndexPath)
        return cell
    }

}

class TableViewCell: UITableViewCell {

    @IBOutlet weak var textField: UITextField!

    override func awakeFromNib() {
        super.awakeFromNib()
        textField.delegate = self
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

}

extension TableViewCell: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

実装詳細

はじめに、Swiftで準備してある下記関数でタッチイベントを取得し、キーボードを閉じようと試みましたが、処理が呼ばれませんでした。調べるとUIScrollViewUITableViewはデフォルトではタッチイベントを取得できないようになっているらしいです。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    self.view.endEditing(true)
}

上記の方法がダメだったので解決策として、UITapGestureRecognizerを使ってタップを認識できるようにし。キーボードを閉じるようにしました。

// タップ認識するためのインスタンスを生成
let tapGesture = UITapGestureRecognizer(
    target: self,
    action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)
// キーボードと閉じる際の処理
@objc public func dismissKeyboard() {
    view.endEditing(true)
}

UITapGestureRecognizerを設定すると他ViewへのTapを認識しなくなります。理由としましては、cancelsTouchesInViewがデフォルトでtrueになっているからだそうで、下記コードを追加することで解消されます。

tapGesture.cancelsTouchesInView = false

個人的な意見にはなりますが、オリジナルアプリの登録画面の実装の際にこの問題に直面しましたが、キーボードが表示されている間はフォーカスがあたっているTextViewTextField以外は触れなくしててもいいかなと考えています。

参考

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

Swift で AWS Lambda 関数を作成する

swift-aws-lambda-runtime を利用して、Swift で AWS Lambda 関数を作成・実行できます。
参考にさせて頂いたサイトとステップを合わせて進めていきます。

参考サイトと AWS の画面が変更されていて少し戸惑いましたので、画像を多めにしています。
また、私がプログラム開発初心者で備忘録も兼ねていますので、文章が長く感じるかもしれません??
もし誤りがあれば指摘して頂けたらありがたいです。

ステップ1: 事前準備

Lambda 関数を作成する環境の用意です。macOS を想定しています。

  • macOS (私の場合:10.15.7)
  • Swift 5.2 以上
  • Docker
  • AWS アカウント(無くてもローカルでは出来ます‼️)

ステップ2: Swift プロジェクトの作成

まずは Lambda 関数を作成するための Swift プロジェクトを作成します。
ターミナルを開いて下記を入力します。
「 SwiftLambdaApp 」はプロジェクト名なので任意で大丈夫です。

ターミナル
~ $ cd ~/Desktop
Desktop $ mkdir SwiftLambdaApp
Desktop $ cd SwiftLambdaApp
SwiftLambdaApp $ swift package init --type executable --name SwiftLambdaApp
SwiftLambdaApp $ open Package.swift


Xcode が起動し Package.swift ファイルが開いたと思います。
Package.swift を編集し、AWS Lambda の接続に必要なライブラリを読み込ませます。

Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription

let package = Package(
    name: "SwiftLambdaApp",
    platforms: [
       .macOS(.v10_15)
    ],
    products: [
         .executable(name: "SwiftLambdaApp", targets: ["SwiftLambdaApp"]),
       ],
    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from:"0.3.0"))
    ],
    targets: [
        .target(
            name: "SwiftLambdaApp",
            dependencies: [
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime")
            ]
        ),
        .testTarget(
            name: "SwiftLambdaAppTests",
            dependencies: ["SwiftLambdaApp"]),
    ]
)

インターネットに接続されていれば、プロジェクトを保存( cmd + S )した直後にライブラリのダウンロードが自動で始まります。

コードの意味を簡単に説明します。

  • 一番上の行: このプロジェクトに必要な Swift の最小バージョンを表しています。

    • 私の環境では Swift 5.3 が既にインストールされているため「 5.3 」ですが、導入した swift-lambda-runtime の Package.swift の一番上の行を確認すると、「 5.2 」以上であれば使用出来る事が分かります。
  • platforms: このプロジェクトが実行できる Apple Platform を定義します。    

    • ここも私の環境では macOS 10.15 ですが、参考サイトでは 10.13 ですので、それ以上なら大丈夫です。
  • dependencies:ここに使用するライブラリを記述します。

    • 参考サイトでは 0.2.0 ですが、現在の最新版は 0.3.0 です。upToNextMajor となっていますので、バージョン1になるまでの最新版が使用されます。

ステップ3: Lambda 関数の作成

続いて、main.swift を編集します。
作成する Lambda 関数は、数値を一つ渡すと、その2乗を返す関数です。

main.swift
import AWSLambdaRuntime

struct Input: Codable {
  let number: Double
}

struct Output: Codable {
  let result: Double
}

Lambda.run { (context, input: Input, callback: @escaping (Result<Output, Error>) -> Void) in
  callback(.success(Output(result: input.number * input.number)))
}

ここでもコードの意味を確認します。

  • Lambda.run 関数を実行する事で、Lambda runtime を呼び出す( Listen 状態にする)

  • Input 構造体:Lambda 関数に渡すパラメータの設定

  • Output 構造体: Lambda 関数の出力値を設定

  • Input, Output を Codable Protocol に準拠させる事で、入力パラメータ、出力値を JSON に対応させる

ステップ4: 作成した Lambda 関数をテストする

作成した Lambda 関数がローカル環境で正しく動くか確認します。
Lambda がこちらのリクエストを受け付けてくれるように、環境変数の設定を行います。

「 Xcode のメニューバー 」 --> 「 Product 」 --> 「 Scheme 」 --> 「 Edit Scheme ... 」をクリック

184.png

開いた画面の左サイドメニューから「 Run 」 --> 「 Argments タブ 」 --> 「 Environment Variables 」と選択し、

  • 「 name 」 ・・・ LOCAL_LAMBDA_SERVER_ENABLED  
  • 「 Value 」 ・・・ true

を追加設定します。

198.png

実行のための設定はひとまず完了ですので、先ほどのプロジェクトを Xcode 上で実行します。

コンソール上で下記のように出力されればOKです。

Xcode上のコンソール
2020-09-30T17:53:08+0900 info LocalLambdaServer : LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke
2020-09-30T17:53:08+0900 info Lambda : lambda lifecycle starting with Configuration
  General(logLevel: info))
  Lifecycle(id: 42441157043799, maxTimes: 0, stopSignal: TERM)
  RuntimeEngine(ip: 127.0.0.1, port: 7000, keepAlive: true, requestTimeout: nil

ターミナルを開いて次の curl コマンドを実行します。

ターミナル
~ $ curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"number": 3}' \
  http://localhost:7000/invoke

すぐに

ターミナル
{"result": 9}

と返ってくればOKです!

ステップ5: Amazon Linux 用にビルドする

今は作成した Lambda 関数をローカルPCの Mac で実行していますが、最終的には Web 上の AWS Lambda に置いて実行します。
AWS Lambda は Amazon Linux2 という OS なので、そこで実行できるようにビルドする必要があります。
そこで、その環境を Docker で構築します。
適当な場所に「Dockerfile」という名前のファイルを作成して、エディタを開いて下記を入力します。

(参考)

Dockerfile
FROM swift:5.3-amazonlinux2

RUN yum -y install \
     libuuid-devel \
     libicu-devel \
     libedit-devel \
     libxml2-devel \
     sqlite-devel \
     python-devel \
     ncurses-devel \
     curl-devel \
     openssl-devel \
     libtool \
     jq \
     tar \
     zip


この Dockerfile を使って Amazon Linux2 用 Swift の Docker イメージを作成します。
作成した Dockerfile と同じ階層で下記を実行します。

ターミナル
$ docker build -t swift-lambda-builder .


Swift プロジェクトのディレクトリに戻って次を実行して、Amazon Linux2 用にビルドします。

ターミナル
SwiftLambdaApp $ docker run \
     --rm \
     --volume "$(pwd)/:/src" \
     --workdir "/src/" \
     swift-lambda-builder \
     swift build --product SwiftLambdaApp -c release


せっくなので docker コマンドの意味も記載します。

docker run [オプション] イメージ [コマンド] [引数...]: Docker イメージからコンテナを作成 & 起動

  • --rm: コンテナは終了時に削除される

  • --volume "$(pwd)/:/src": Docker 側の /src ディレクトリを Mac 側 の カレントディレクトリ ( $(pwd) )にマウント(ディレクトリの共有)

  • --workdir "/src/":/src ディレクトリを作業用のディレクトリとする

  • 「 swift-lambda-builder 」:「 swift-lambda-builder 」イメージからコンテナ作成

  • swift build --product SwiftLambdaApp -c release
    作成したコンテナで、Swift コマンド 「 swift build --product SwiftLambdaApp -c release 」を実行する。
    この場合は、

ステップ6: デプロイ用に実行可能ファイルをパックする

Amazon Linux2 用の実行可能ファイルは作成しましたが、これだけではまだ Amazon Linux2 で実行できません。

AWS Lambda の環境は Swift が使えない状態なので、先ほどビルドしたファイルを実行するためには Swift ランタイムライブラリが必要です。

「ステップ5でビルドしたファイル」と「 Swift ランタイムライブラリ」を一緒に zip ファイルにして、AWS Lambda へアップロードします。

参考サイトではこのためのスクリプトを用意してくれています。⭐️⭐️✨

まずスクリプトを保存するためのディレクトリを用意します。


Swift プロジェクトの続きで、ターミナルに下記を入力します。

ターミナル
SwiftLambdaApp $ mkdir scripts
SwiftLambdaApp $ cd scripts


エディタを開いて下記を入力して、package.sh という名前で保存します。

テキストエディタ(ファイル名「package.sh」で保存)
#!/bin/bash

set -eu

executable=$1

target=.build/lambda/$executable
rm -rf "$target"
mkdir -p "$target"
cp ".build/release/$executable" "$target/"
# add the target deps based on ldd
ldd ".build/release/$executable" | grep swift | awk '{print $3}' | xargs cp -Lv -t "$target"
cd "$target"
ln -s "$executable" "bootstrap"
zip --symlinks lambda.zip *

スクリプト大解剖開始‼️?

  • set -eu

    • set [ オプション ]: シェルの設定を確認、変更する
    • e: シェルコマンド中でエラーが起きた場合、直ちにシェルを終了する
    • u: 定義していない変数を使用した場合エラーとする
  • executable=$1

    • $1: スクリプト実行時のコマンドライン引数の1番目を取得
    • executable=$1:「executable」という名前の変数を定義 & $1 を代入
  • target=.build/lambda/$executable

    • $executable:上記で定義した変数 executable を参照
  • rm -rf "$target"

    • 変数 target で参照しているディレクトリを再帰的に削除
  • mkdir -p "$target"

    • p: 作成するディレクトリの親ディレクトリが無ければ作成
  • ldd ".build/release/$executable" | grep swift | awk '{print $3}' | xargs cp -Lv -t "$target"

    • ldd [ オプション ] プログラム:指定したプログラムに必要な共有ライブラリを表示
    • grep [ オプション ] 検索パターン ファイル: 指定したファイルの中で「文字列(検索)パターン」が含まれている行を表示 ( コマンド | grep [ オプション ] 検索パターン )
    • awk [ オプション ] [ コマンド ] [ ファイル・・・ ]: スペースで区切られた文字列に対して [ コマンド ] 処理を行う

      • awk '{print $3}': 文字列の3番目の引数を表示
    • xargs [ オプション ] [ コマンド ] [ コマンドの引数 ]: 標準入力やファイルからリストを読み込み、コマンドラインを作成して実行

      • xargs cp -Lv -t "$target": awk コマンドによって取得した ファイル名(参照先)を、cp コマンドで target へコピー
    • cp [ オプション ] [ コピー元・・・ ] [ コピー先 ]

      • L オプション: コピー元のシンボリックリンクを常にたどる
      • v オプション: 実行内容を表示する
      • t オプション: 「t」で指定したディレクトリにコピーする
  • ln -s "$executable" "bootstrap"

    • ln [ オプション ] [ ファイル名 ] [ リンク名 ]: ファイルのハードリンク、もしくはシンボリックリンクを作成
    • シンボリックリンク: Windows のショートカットに該当するリンク

      • リンク元のファイルが移動、削除されると、リンク元のファイルの実体にアクセスできない。
      • 異なるファイルシステム上のファイルやディレクトリにリンクできる。
    • ハードリンク: 1つのファイルの実体を複数のファイル名で表せるリンク。同じファイルの実体を共有する方式。

      • リンク元のファイルが移動、削除されても、ファイルの実体にはアクセスできる。
      • 異なるファイルシステム上のファイルやディレクトリにリンクできない。
    • s: シンボリックリンクを作成(指定しないとハードリンクを作成)

  • zip --symlinks lambda.zip *

    • zip [ オプション ] [ ZIP ファイル名 ] [ 圧縮対象ファイル ]: 指定したファイルを ZIP 形式で保存する。
    • symlinks:シンボリックリンクをたどらずに、シンボリックリンクのまま格納する。

カレントディレクトリにある全てのファイル(*)を 「lambda.zip」という名前のファイルに圧縮する。このときシンボリックリンクのファイルはそのままで圧縮する。こうしないと、実ファイルとして圧縮されてしまう(リンクで十分)

・・・

スクリプト大解剖完了‼️??


では「 SwiftLambdaApp 」を引数にしてスクリプトを実行します。

ターミナル
SwiftLambdaApp $ docker run \
    --rm \
    --volume "$(pwd)/:/src" \
    --workdir "/src/" \
    swift-lambda-builder \
    scripts/package.sh SwiftLambdaApp

作成した 「 .build 」フォルダは隠しフォルダなので非表示になっています。
Finder で「Cmd + Shift + .(ドット)」を同時に押す事で表示されます。
また、アクセスが拒否されたというエラーが表示された場合は、ファイルの実行を許可してください。

ターミナルのエラー内容
docker: Error response from daemon: 
OCI runtime create failed: 
container_linux.go:349: 
starting container process caused "exec: \"scripts/package.sh\": permission denied": unknown.
ターミナル(shファイルに権限を与える)
$ chmod +x scripts/package.sh

ステップ7: AWS で Lambda を作成・実行

ここからは AWS アカウントがある場合に進められます。
AWS コンソール でログインして Lambda の設定画面を開きます。

569.png

Lambda の設定画面から関数を作成していきます。
「関数の作成」ボタンを押します。

575.png

開いた画面で次を入力、選択します。

  • 「一から作成」を選択

  • 関数名:任意で大丈夫です。下記では「 SwiftLambdaApp 」

  • ランタイム:「Amazon Linux 2 でユーザー独自のブートストラップを提供する」

入力できたら「関数の作成」ボタンを押します。

587.png

しばらくすると下記の画面が表示されます。

591.png

これで Lambda 関数の登録が出来ました。
まだ Lambda 関数の中身がありませんので、作成した zip ファイルをアップロードします。
少し下にスクロールして「関数コード」項目の「アクション」を押します。

599.png

「zip ファイルをアップロード」を選択して先ほどの zip ファイルを選択します。
隠しファイルのアクセス方法は、Finder で「Cmd + Shift + .(ドット)」です。

605.png

( 隠しファイルが非表示の状態の Finder )

609.png

「Cmd + Shift + .(ドット)」で、隠しファイル表示すると・・・
(ここまでの手順通りでしたら、.git、.gitignore は無くて大丈夫です)

615.png

zip ファイル選択

619.png

最後に Lambda 関数に対するパラメータを設定します。これは main.swift に記述した Input 構造体に相当します。

Lambda 関数の画面右上(この文章少し上の緑のバーがある画像)の「テスト」ボタンを押して、下記のようにします。

625.png

「作成」ボタンを押して設定は完了です。
もう一度「テスト」ボタンを押して、次のような画面が出れば OK です???

631.png

終わりに

私のような初心者でも結構簡単に使えて楽しかったです。
他の AWS サービスとの連携も出来るようなので、せっかくなので何か作ってみたいと思います。
ご覧頂いた方々も是非何か作ってみてください?

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

SwiftUIでプレビューできるデバイスの一覧

目的

SwiftUIでは開発中の画面が実際にどう表示されるのかをプレビューする機能があります。

その際、プレビューすることができる端末を指定することができますが、デフォルトだとrawValueで指定する方法しかなく、プレビューしたい端末が見つからない&タイポの温床などの問題があるため、端末のリストを作成しました。

環境

  • Xcode12
  • Swift5

リスト

enum DeviceList: String {
    case iPhone_4s = "iPhone 4s"
    case iPhone_5 = "iPhone 5"
    case iPhone_5s = "iPhone 5s"
    case iPhone_6_Plus = "iPhone 6 Plus"
    case iPhone_6 = "iPhone 6"
    case iPhone_6s = "iPhone 6s"
    case iPhone_6s_Plus = "iPhone 6s Plus"
    case iPhone_SE_1st_generation = "iPhone SE (1st generation)"
    case iPhone_7 = "iPhone 7"
    case iPhone_7_Plus = "iPhone 7 Plus"
    case iPhone_8 = "iPhone 8"
    case iPhone_8_Plus = "iPhone 8 Plus"
    case iPhone_X = "iPhone X"
    case iPhone_Xs = "iPhone Xs"
    case iPhone_Xs_Max = "iPhone Xs Max"
    case iPhone_Xʀ = "iPhone Xʀ"
    case iPhone_11 = "iPhone 11"
    case iPhone_11_Pro = "iPhone 11 Pro"
    case iPhone_11_Pro_Max = "iPhone 11 Pro Max"
    case iPhone_SE_2nd_generation = "iPhone SE (2nd generation)"
    case iPod_touch_7th_generation = "iPod touch (7th generation)"
    case iPad_2 = "iPad 2"
    case iPad_Retina = "iPad Retina"
    case iPad_Air = "iPad Air"
    case iPad_mini_2 = "iPad mini 2"
    case iPad_mini_3 = "iPad mini 3"
    case iPad_mini_4 = "iPad mini 4"
    case iPad_Air_2 = "iPad Air 2"
    case iPad_Pro_9_7inch = "iPad Pro (9.7-inch)"
    case iPad_Pro_12_9inch_1st_generation = "iPad Pro (12.9-inch) (1st generation)"
    case iPad_5th_generation = "iPad (5th generation)"
    case iPad_Pro_12_9inch_2nd_generation = "iPad Pro (12.9-inch) (2nd generation)"
    case iPad_Pro_10_5inch = "iPad Pro (10.5-inch)"
    case iPad_6th_generation = "iPad (6th generation)"
    case iPad_7th_generation = "iPad (7th generation)"
    case iPad_Pro_11inch_1st_generation = "iPad Pro (11-inch) (1st generation)"
    case iPad_Pro_12_9inch_3rd_generation = "iPad Pro (12.9-inch) (3rd generation)"
    case iPad_Pro_11inch_2nd_generation = "iPad Pro (11-inch) (2nd generation)"
    case iPad_Pro_12_9inch_4th_generation = "iPad Pro (12.9-inch) (4th generation)"
    case iPad_mini_5th_generation = "iPad mini (5th generation)"
    case iPad_Air_3rd_generation = "iPad Air (3rd generation)"
    case iPad_8th_generation = "iPad (8th generation)"
    case iPad_Air_4th_generation = "iPad Air (4th generation)"
    case Apple_TV = "Apple TV"
    case Apple_TV_4K = "Apple TV 4K"
    case Apple_TV_4K_at_1080p = "Apple TV 4K (at 1080p)"
    case Apple_Watch__38mm = "Apple Watch - 38mm"
    case Apple_Watch__42mm = "Apple Watch - 42mm"
    case Apple_Watch_Series_2__38mm = "Apple Watch Series 2 - 38mm"
    case Apple_Watch_Series_2__42mm = "Apple Watch Series 2 - 42mm"
    case Apple_Watch_Series_3__38mm = "Apple Watch Series 3 - 38mm"
    case Apple_Watch_Series_3__42mm = "Apple Watch Series 3 - 42mm"
    case Apple_Watch_Series_4__40mm = "Apple Watch Series 4 - 40mm"
    case Apple_Watch_Series_4__44mm = "Apple Watch Series 4 - 44mm"
    case Apple_Watch_Series_5__40mm = "Apple Watch Series 5 - 40mm"
    case Apple_Watch_Series_5__44mm = "Apple Watch Series 5 - 44mm"
    case Apple_Watch_SE__40mm = "Apple Watch SE - 40mm"
    case Apple_Watch_SE__44mm = "Apple Watch SE - 44mm"
    case Apple_Watch_Series_6__40mm = "Apple Watch Series 6 - 40mm"
    case Apple_Watch_Series_6__44mm = "Apple Watch Series 6 - 44mm"
}

情報取得元

公式ドキュメントに寄れば、下記のコマンドでプレビューできる端末のリストを取得することができます。
$ xcrun simctl list devicetypes

取得したdeviceのリストを下記の方法で加工し、上記のリストを作成しました。正規表現など初心者なのでお見苦しいですが...
$ xcrun simctl list devicetypes | grep -v "Device Types" | sed -e "s/\((\)\(com\.apple\.CoreSimulator\.SimDeviceType\.*\)\([0-9a-zA-Z\-]*\)\()$\)//g"

import Foundation
var rawText = """ //上記コマンドの結果
iPhone 4s
iPhone 5
iPhone 5s
iPhone 6 Plus
iPhone 6
iPhone 6s
iPhone 6s Plus
iPhone SE (1st generation)
iPhone 7
...
"""
    .components(separatedBy: "\n")
var body = "enum DeviceList: String {\n"
rawText.forEach {
    let rawDevice = $0
        .components(separatedBy: " ")
        .map { $0.replacingOccurrences(of: "-", with: "") }
        .map { $0.replacingOccurrences(of: "(", with: "") }
        .map { $0.replacingOccurrences(of: ")", with: "") }
        .map { $0.replacingOccurrences(of: ".", with: "_") }
        .joined(separator: "_")    
    body.append("    case \(rawDevice) = \"\($0)\"\n")
}
body.append("}")
print(body)

参考

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

【SwiftUI】ScrollViewのScrollsToTopを無効にする方法

ScrollsToTopとは

ScrollViewが表示されている状態でStatusBar(一番上の時刻やバッテリー残量が表示されている部分)をタップすると自動で一番上までスクロールしてくれる機能。

デフォルトではオンになっている。

無効にする方法

UIScrollView.appearance().scrollsToTop = falseを記述する。

例:

ScrollView {
  ...
}.onAppear {
  UIScrollView.appearance().scrollsToTop = false
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftのwillSet didSetの使い分けについて考えてみた。

Swift歴3ヶ月の初心者の初投稿

独学でSwiftを学習する中、覚えた知識をアウトプットにより血肉にしていきます。
アウトプットする事自体始めてで、アウトプットの練習も兼ねております。

今回作った課題

各辺の長さがInt型の直方体のクラスをリファクタリングしながら学びます。

目的

覚えた知識を実践に近い形でコーディングすることで使いこなせるようになること。

  • willSetdidSetの有用性について
  • コンピューティッドプロパティを利用したRead Only Propertyの有用性
  • DRY原則違反の解消

条件:どんな直方体?

  • Int型の縦横高さのプロパティを持っている
  • 一辺の最大は6
  • Int型の体積のプロパティを持っている
  • 体積によって変わるenum型のサイズ(S,M,L)プロパティを持つ
  • サイズの条件はS(1...8) M(9...64) L(65...216)

初期コード

※検証用に printStatus()という関数も用意しておきます。

class IntBox{
    enum Size{
        case S, M, L
    }

    // 横縦高さ
    var width: Int
    var height: Int
    var depth: Int

    // 体積
    var volume: Int

    // サイズ
    var size:Size

    init(w: Int, h: Int, d: Int){
    // 横縦高さの初期化
        width = w
        height = h
        depth = d

    // 体積の初期化
        volume = w * h * d

    // サイズの初期化
        switch volume{
        case 1...8:
            size = .S
        case 9...64:
            size = .M
        default:
            size = .L
        }
    }

    // 検証用にステータス一覧を表示する関数を用意しています。
    func printStatus(){
        var status = """
        ---ステータス---
        width: \(width)
        height: \(height)
        depth: \(depth)
        volume: \(volume)
        size: \(size)
        """
        print(status)
    }
}

突っ込みどころ

  1. 一辺の最大6という条件が無視されている。
  2. 一辺の長さが0以下の場合にエラーになってくれない。
  3. 辺の長さを変えても体積やサイズは変わらない。
  4. 体積を外から変更できてしまう。
  5. サイズを外から変更できてしまう。

検証

let cb = IntBox(w: 1, h: 1, d: 1)
cb.printStatus()

// 横幅を変えてみる
cb.width = 11
cb.printStatus()

// 体積を変えてみる
cb.volume = -20
cb.printStatus()

// サイズを変えてみる
cb.size = .L
cb.printStatus()

実行結果

---ステータス---
width: 1
height: 1
depth: 1
volume: 1
size: S

---ステータス---
width: 11
height: 1
depth: 1
volume: 1 # 変化すべし!!
size: S

---ステータス---
width: 11
height: 1
depth: 1
volume: -20 # 0以下はダメ setできるのもおかしい(体積は各辺の長さに依存している)
size: S

---ステータス---
width: 11
height: 1
depth: 1
volume: -20
size: L # 体積と同じくsetできるのがおかしい。依存関係を考えるべし

ということで一個ずつ解決していきます。

#1 バリデーション

突っ込みどころ 1, 2を解決します。

1 一辺の最大6という条件が無視されている。

2 一辺の長さが0以下の場合にエラーになってくれない。

イニシャライズの最初に下記を挿入します。

if w <= 0 || w > 6 {fatalError("w:\(w) not in 1...6")}
if h <= 0 || h > 6 {fatalError("h:\(h) not in 1...6")}
if d <= 0 || d > 6 {fatalError("d:\(d) not in 1...6")}

#2 willSet と didSet

突っ込みどころ 3を解決します。

3 辺の長さを変えても体積やサイズは変わらない。

縦横高さの値が変化時に体積やサイズを変更したいので
各プロパティにwillSetdidSetを追加してそこで体積やサイズをセットします。

will did どちらのタイミングに何をするのか

バリデーションは変更したい長さをセットしてもいいかどうかの検証を意味するのでセットする前に。
長さの変更が原因で体積やサイズが変更するのでセット後に。

  • willSet: バリデーション
  • didSet: 体積やサイズのセット
var width: Int{
    willSet{
        if newValue<= 0 || newValue> 6 {fatalError("<width: \(newValue)> not in 1...6")}
    }
    didSet{
        _volume = width * height * depth
        switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
        }
    }
}

var height: Int{
    willSet{
        if newValue<= 0 || newValue> 6 {fatalError("<height: \(newValue)> not in 1...6")}
    }
    didSet{
        _volume = width * height * depth
        switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
        }
    }
}

var depth: Int{
    willSet{
        if newValue<= 0 || newValue> 6 {fatalError("<depth: \(newValue)> not in 1...6")}
    }
    didSet{
        _volume = width * height * depth
        switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
        }
    }
}

#3 Read Only Property にする。

突っ込みどころ 4と5を解決します。

4 体積を外から変更できてしまう。
5 サイズを外から変更できてしまう。

Read Only Property にすることでsetができなくしちゃいます。

  • volume と size に get{}を追加
  • 外から見ることができないprivate var _volume と private var _size にデータを保持します。
private var _volume: Int
private var _size: Size

var volume: Int {
    get {
        return _volume
    }
}

var size: Size {
    get {
        return _size
    }
}

一旦完成!?

。。。想像通りの冗長なコードになってます。

class IntBox{
    enum Size{
        case S, M, L
    }
    private var _volume: Int
    private var _size: Size

    var width: Int{
        willSet{
            if newValue<= 0 || newValue> 6 {fatalError("<width: \(newValue)> not in 1...6")}
        }
        didSet{
            _volume = width * height * depth
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
            }
        }
    }

    var height: Int{
        willSet{
            if newValue<= 0 || newValue> 6 {fatalError("<height: \(newValue)> not in 1...6")}
        }
        didSet{
            _volume = width * height * depth
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
            }
        }
    }

    var depth: Int{
        willSet{
            if newValue<= 0 || newValue> 6 {fatalError("<depth: \(newValue)> not in 1...6")}
        }
        didSet{
            _volume = width * height * depth
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
            }
        }
    }

    var volume: Int {
        get {
            return _volume
        }
    }

    var size: Size {
        get {
            return _size
        }
    }

    init(w: Int, h: Int, d: Int){
        if w <= 0 || w > 6 {fatalError("<w: \(w)> not in 1...6")}
        if h <= 0 || h > 6 {fatalError("<h: \(h)> not in 1...6")}
        if d <= 0 || d > 6 {fatalError("<d: \(d)> not in 1...6")}
        width = w
        height = h
        depth = d
        _volume = w * h * d
        switch _volume{
        case 1...8:
            _size = .S
        case 9..<64:
            _size = .M
        default:
            _size = .L
        }
    }

    func printStatus(){
        var status = """
        ---ステータス---
        width: \(width)
        height: \(height)
        depth: \(depth)
        volume: \(volume)
        size: \(size)
        """
        print(status)
    }
}

let cb = IntBox(w: 1, h: 1, d: 1)
cb.printStatus()

// 横幅を変えてみる
cb.width = 11
cb.printStatus()

DRY原則違反が多いので共通の関数を作ってまとめていこうと思います。

#4 DRY原則適用

※関数にまとめる上ですべてのストアドプロパティが初期化しないとself.func が呼び出せないので各プロパティに初期値を設定しておきます。
→本当はもっといい方法があるのかも

バリデート部分を関数にまとめるべし

private func lengthValidate(_ length: Int, _ name: String = "length"){
    if length <= 0 || length > 6 { fatalError("<\(name): \(length)> not in 1...6") }
}

体積の計算やサイズの評価をまとめるべし

private func setStatus() {
    setVolume()
    setSize()
}

private func setVolume() {
    _volume = width * height * depth
}

private func setSize(){
    switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
    }
}

縦横高さを変更する度にsetStatus()が呼ばれるのも解消したい。

volumeやsizeが参照される時にsetStatus()ができればいいと思います。
ただし変更がないのに
volumeやsizeが参照される度にsetStatus()が呼ばれるのも回避したいので
縦横高さを変更後一度だけsetStatus()を呼びたいので
private var shouldSetStatus: Boolを作成してsetStatus()を呼ぶかどうか評価します。

private var shouldSetStatus: Bool

var width: Int = 1 {
    willSet { lengthValidate(newValue, "width") }
    didSet { shouldSetStatus = true } // <- 変更
}
...

var volume: Int {
    get {
        if shouldSetStatus { setStatus() }
        return _volume
    }
}
...

private func setStatus() {
    setVolume()
    setSize()
    shouldSetStatus = false // <- 追加
}

DRY原則違反の解決後コード

class IntBox{
    enum Size{
        case S, M, L
    }

    private var _volume: Int = 1
    private var _size: Size = .S
    private var shouldSetStatus: Bool = false

    var width: Int = 1 {
        willSet { lengthValidate(newValue, "width") }
        didSet { shouldSetStatus = true }
    }
    var height: Int = 1 {
        willSet { lengthValidate(newValue, "height") }
        didSet { shouldSetStatus = true }
    }
    var depth: Int = 1 {
        willSet { lengthValidate(newValue, "depth") }
        didSet { shouldSetStatus = true }
    }

    var volume: Int {
        get {
            if shouldSetStatus { setStatus() }
            return _volume
        }
    }
    var size: Size {
        get {
            if shouldSetStatus { setStatus() }
            return _size
        }
    }

    init(w: Int, h: Int, d: Int){
        lengthValidate(w, "w")
        lengthValidate(h, "h")
        lengthValidate(d, "d")
        width = w
        height = h
        depth = d
        setStatus()
    }

    private func lengthValidate(_ length: Int, _ name: String = "length"){
        if length <= 0 || length > 6 { fatalError("<\(name): \(length)> not in 1...6") }
    }

    private func setStatus() {
        setVolume()
        setSize()
        shouldSetStatus = false
    }

    private func setVolume() {
        _volume = width * height * depth
    }

    private func setSize(){
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
        }
    }

    // 検証用コード
    func printStatus(){
        let status = """
        ---ステータス---
        width: \(width)
        height: \(height)
        depth: \(depth)
        volume: \(volume)
        size: \(size)
        """
        print(status)
    }
}

// 検証
let cb = IntBox(w: 1, h: 2, d: 3)
cb.printStatus()

// 横幅を変えてみる
cb.width = 5
cb.printStatus()

最後に

本来は一辺の最大値やS,M,Lの評価を柔軟に変更できるよう抽象的なプロトコルなどから設計したり
エラーに関してOptionalを使わずにfatalErrorのみになってますが、イニシャライズ時はエラーの際にnilを返すとか長さ変更時のエラーとは分けた方がいいのかもしれない。。。
一旦、今の自分の精一杯を記事にしてみました。
いつかこれが恥ずかしい記事と思えるほど成長しようと思います。
あとこの記事に2時間×3日かけてしまった。。。
コスパも考えて記事を書いていこうと思います。

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

文字列を代入する時に改行やタブをたくさん入れたい時の小技

for 変数 in 開始値 ... 終了値 {
                            // ステートメント
                        }

こういう文字列を変数に代入して出力したい時、
スクリーンショット 2020-10-24 12.04.39.png
こんな風に書いてました。醜い・・・。

スクリーンショット 2020-10-24 12.04.18.png
こっちの方がソースコードがみやすいですね。

let string = """
             ここに入れたい文字列をかく。
            こんな風にタブ入力が可能になる。
    ここにかくことはできない。"""の後以降にかけば認識される。
        """
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xibファイルの高さを自動調整する

XibファイルをViewに埋め込んだ時に、高さを文字数などによって自動調整したい場合の方法。

非自動調整時:
緑のViewが途中で切れてしまっている。
Simulator Screen Shot - iPhone 11 Pro Max - 2020-10-24 at 11.52.06.png

自動調整後:
緑のViewが最後まで表示されている。
Simulator Screen Shot - iPhone 11 Pro Max - 2020-10-24 at 11.52.46.png

【解決方法】
StoryBoard上で埋め込んでいるXibファイルを表示するViewの設定を下記のように変更。
Layout→Autoresizing Mask
スクリーンショット 2020-10-24 11.49.59.png
Autoresizingで自動調整して欲しい拘束を設定。
スクリーンショット 2020-10-24 11.50.08.png

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

Xcodeでプロジェクト作成時に.gitignoreを追加する

概要

Xcodeでプロジェクトを作成した際に、.gitignoreが作成されないままinitialコミットされるので、.gitignoreを追加して再コミットする。

手順

.gitignoreの生成

以下のサイトでXcode用の.gitignoreを生成し、プロジェクトに追加する。
https://www.toptal.com/developers/gitignore

initial commitの取消

initial commitはgit resetではなく、以下のコマンドで取り消し可能。
取り消し後、再度コミットし直す。

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

Xcode12.0.1でXVim2を利用する

概要

Xcodeをvimのキーバインドで利用するため、XVim2をインストールする。
https://github.com/XVimProject/XVim2

手順

1. 証明書の作成

READMEのINSTALL.1にあるリンク「 You can read the instructions for how to do this and...」の通り実行する。
https://github.com/XVimProject/XVim2/blob/master/SIGNING_Xcode.md

キーチェーンアクセスを開き、メニューバーから「証明書アシスタント > 証明書を作成」を選択する。
スクリーンショット 2020-10-24 10.07.21.png
以下の通り入力する。
スクリーンショット 2020-10-24 10.09.33.png
コマンドラインから署名を実行する。(時間がかかるので放置。その後、ログインパスワードの入力を何度か求められる。)

sudo codesign -f -s XcodeSigner /Applications/Xcode.app

2. Xvim2リポジトリのclone

git clone https://github.com/XVimProject/XVim2.git

3. ブランチ切り替え

Xcodeのバージョンに合わせたブランチに切り替える。
ブランチのリストは以下の通り。
https://github.com/XVimProject/XVim2#branches-and-releases
※ 2020.10.24時点ではmasterブランチがXcode12に対応しているので、Xcode12を利用しているのであれば切り替える必要は無い。

ここで、以下の結果が空の場合はREADMEの手順通り設定を行う。「/Applications/Xcode.app/Contents/Developer」というようにパスが表示されれば問題無い。

xcode-select -p

4. make実行

cloneしてきたXVim2のディレクトリ内でmakeを実行する。

cd XVim2
make

5. Xcode起動

Xcodeを起動すると、以下のダイアログが表示されるので、「Load Bundle」を選択する。
スクリーンショット 2020-10-24 10.42.53.png

※ 間違えて「Skip Bundle」を選択した場合は、Xcodeを閉じた上で以下のコマンドを実行する。(Xcode 12.0.1の場合のコマンド。最後のバージョンの指定はXcodeに合わせる。)

defaults delete com.apple.dt.Xcode DVTPlugInManagerNonApplePlugIns-Xcode-12.0.1

6. Xcode再起動

Xcodeを一度閉じて、再起動する。
成功すると、以下のようにカーソルがブロック型となり、Vimのキーバインドで操作が可能となる。
スクリーンショット 2020-10-24 10.50.51.png

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