Open56

iOSアプリ開発覚書

ぱむっとぱむっと

AutoLayout FirstItemとSecondItemの意味

FirestItemからSecondItemに対してConstantをカウントしているということ!

例えば
Traling Aliment Constraintに対して
FirstItemのLeadingに対して制約をつけるかTralingに対して制約をつけるか選択できる。
SecontItemは制約の到達先でLeading、Traling CenterXとか選択できる。

ぱむっとぱむっと

タイマーの作り方

Swift 5.1
Xcode 11.4

最小限の書き方

Timerクラスとは?時間の経過とともに
指定されたメソッドを呼び出して処理する仕組み。
#selecterで指定されたメソッドを呼び出す。
timeInterval: は、呼び出す時間間隔
repeats: は、その時間間隔で呼び出しを繰り返す。
userInfo:  は、オブジェストにつける値という意味のようだが意味不明。

必ずinvalidate()を実行してTimarインスタンスを破棄しないと
メモリートラブルになる可能性がある。

//タイマークラスのインスタンスを生成(必ず必要)
    var timer: Timer!


    func startTimer() {
        timer = Timer.scheduledTimer(
            timeInterval: 0.01,
            target: self,
            selector: #selector(self.timerCounter),
            userInfo: nil,
            repeats: true)
    }


func stopTimer {
timer.invalidate()
}

import UIKit
 
class ViewController: UIViewController {
 
//タイマークラスのインスタンスを生成(必ず必要)
    var timer: Timer!
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
    }
 
// viewの生成が終わったらタイマーがスタートされる。
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(true)
 
        startTimer()
 
    }
 
// 
    func startTimer() {
        timer = Timer.scheduledTimer(
            timeInterval: 0.01,
            target: self,
            selector: #selector(self.timerCounter),
            userInfo: nil,
            repeats: true)
    }
 
    @objc func timerCounter() {
        let now = Date()
 
        let fomatter = DateFormatter()
        fomatter.dateFormat = "mm:ss.SSS"
        print(fomatter.string(from: now))
    }
 
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(true)
        timer.invalidate()
    }
 
}

タイトル2

ぱむっとぱむっと

UIViewControllerのライフサイクル

viewDidLoad

ViewControllerのviewがロードされた後に呼び出される。

viewWillAppear

viewが表示される直前に呼ばれる。

viewDidAppear

完全に遷移が行われ、スクリーン上に表示された時に呼ばれる。

viewWillDisappear:, viewDidDisappear:

viewWillDisappear:viewが表示されなくなる直前に呼び出される。
viewDidDisappear: 完全に遷移が行われ、スクリーン上からViewControllerが表示されなくなったときに呼ばれる。

注意点としては、viewDidDisappear:が呼び出されたからといってviewControllerオブジェクトが破棄されるわけではない。

例えばUITabBarControllerやUINavigationControllerなどに保持され続けている場合はviewControllerは保持される。

ぱむっとぱむっと

| インチ | 端末 | xcode表示 | ポイント|倍率 |ピクセル |
| ---- | ---- | ---- |---- | ---- |---- | ---- |---- | ---- |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |
| Text | Text | Text |Text | Text | Text |Text | Text |

早見表
| インチ| 端末| xcodeの表示| ポイント| 倍率|
| ---- | ---- | ---- |
|3.5| iPhone 1,3G,3GS| 1x |320x480| 1 |320x480|
|3.5| iPhone 4,4s |2x| 320x480| 2 |640x960|
|4.0 |iPhone 5,5s,5c,SE |Retina 4 |320x568| |2| |640x1136|
|4.7| iPhone 6,6s,7,8,SE2| Retina HD 4.7″| 375x667 |2| 750x1334|
|5.4| iPhone 12mini Super Retina XDR |375x812 |3 1080x2340|
|5.5| iPhone 6,6s,7,8 plus| Retina HD 5.5″| |414x736| 3| 1242x2208|
|5.8| iPhone X,XS,11Pro |iPhone X / iPhone XS| |375x812| 3| 1125x2436|
|6.1| iPhone XR,11| iPhone XR| 414x896| 2 |828x1792|
|6.1| iPhone 12,12Pro| Super Retina XDR| 390x844| 3 |1170x2532|
|6.5| iPhone XS,11ProMax| iPhone XS Max| 414x896| 3 |1242x2688|
|6.7| iPhone 12ProMax| Super Retina XDR| 428x926| 3 |1284x2778|

ぱむっとぱむっと

#FireStore/ Storageに画像を保存して読み出す

テストアプリの構成。

  1. カメラで撮影。
  2. 保存を選択
  3. 処理の流れ
  4. Storageに画像を送信
    2.Storageから保存URLが返ってくる。
  5. FireStoreに下記を保存。
  • ID
  • postDate
  • 画像URL
  • メモ
ぱむっとぱむっと

FireBaseStorageセットアップ方法

Firebaseプロジェクトを作成する.

ウイザードに従って
GoogleService-InfoをダウンロードしてXcodeプロジェクトに追加する。

Firebase/Storage のインストール

pod 'Firebase/Storage'

pod install を実行します。

$ pod install
※環境によって、新しい Firebase のライブラリをインストールすると Xcode でリンクエラーが発生することがあります。そういった場合は以下のように一度ライブラリをクリアしてインストールし直すと解消する場合があります。

$ pod deintegrate
$ pod install
Firebase コンソールの Storage 画面を確認
Firebase プロジェクト作成後、コンソール画面の Storage から「始める」をクリックします。

セキュリティ保護ルールについての説明が表示されます。

セキュリティルールについては今回は触れませんが、以下のデフォルト設定では、read、write ともに Firebase Authentication による認証が必要という意味になります。

次へ進むと、ロケーションの設定画面が表示されます。ロケーションの設定はこの一度限で変更不可ですので間違ったロケーションを選ばないように注意しましょう。

東京(asia-northeast1)か大阪(asia-northeast2)が候補となると思いますが、東京を選択しておくのが無難です。

ロケーションの選択についてはこちらのQiita記事が参考になります。

「完了」をクリックすると Storage の Files タブのメニューが表示されます。

Storage は MacOS・Windows・Linux などと同じようにディレクトリの階層構造となっています。

赤い四角で囲んだ部分がこの Storage のルートディレクトリとなります。

gs://[プロジェクト名].appspot.com
[プロジェクト名].appspot.com/ 以下に任意のディレクトリ(以降フォルダとします)を生成し、ファイルを

格納していくことになります。
フォルダの作成と画像の格納
コンソール画面からフォルダを作成し、画像を格納して見ましょう。

フォルダ追加ボタンをクリックし、フォルダ名を入力後「フォルダを追加」をクリックします。

test フォルダが作成されたことが確認できます。

続いて test フォルダをクリックし中身を参照します。当然まだ何も入っていません。

「ファイルをアップロード」をクリックし任意の画像を選択しアップロードしてみましょう。

今回は test.png という画像をアップロードしました。

ファイル名、サイズ、画像の種類、最終更新日が確認できます。

それでは、ブラウザから画像にアクセスしてみましょう。

test.png をクリックすると右側に画像の詳細情報が表示されますので、test.png のリンクをクリックしてみましょう。

以下のように画像にアクセスできていることが確認できるはずです。

Xcode coding

Swift で画像をアップロードする
いよいよ本題です。

Storage の機能を使うには FirebaseStorage をインポートします。

import FirebaseStorage
先程の test.png を同じ場所にアップロードするには以下のような流れとなります。

まず、Firebase Storage のルートフォルダへの参照を以下のように取得します。

let storage = Storage.storage()
let reference = storage.reference()

続いて、画像のアップロードパスの参照を取得します。

let path = "gs://[プロジェクト名].appspot.com/test/test.png"
let imageRef = reference.child(path)

画像データは、ローカル画像を URL 型でアップロードする方法と、UIImage から Data 型に変換して バイナリデータとしてアップロードする方法の2通りがあります。

URL指定でアップロードする場合
putFile を使ってアップロードします。

let url = URL(string: "ローカル(端末)の格納場所")
let uploadTask = imageRef.putFile(from: url)

Data指定でアップロードする場合
putData を使ってアップロードします。画像は予め UIImage から Data へ変換しておきます。

guard let data = image.pngData() else {
    return
}
let uploadTask = imageRef.putData(data)

putFile・putData ともに即座にアップロードは完了しないため、戻り値として取得できる StorageUploadTask を使って、成功 or 失敗を監視する必要があります。

アップロードを監視するには、StorageUploadTask の observe メソッドを使用します。

成功の監視
observe に .success を与えるとアップロード成功を監視できます。

var downloadURL: URL?
uploadTask.observe(.success) { _ in
    imageRef.downloadURL { url, error in
        if let url = url {
            downloadURL = url
        }
    }
}

成功するとクロージャが実行されるので、クロージャ内で画像の参照(imageRef)からダウンロードするURLを取得できます。

URLは後で表示する時に使用するので保持しておきましょう。

失敗の監視
observe に .failure を与えるとアップロード失敗を監視できます。

    uploadTask.observe(.failure) { SnapshotMetadata in
      if let errorMessage = SnapshotMetadata.error as NSError? {
        switch (StorageErrorCode(rawValue: Int(errno))!) {
        case .objectNotFound:
          print("ファイルが存在しません。",errorMessage)
        case .unauthorized :
          print("ユーザーにはファイルにアクセスする権限がありません",errorMessage)
          break
        case .cancelled :
          print("ユーザーがアップロードをキャンセルしました")
        case .unknown :
          print("不明なエラーが発生しました。サーバーの応答を調べてください",errorMessage)
          break
        default:
          print("別のエラーが発生しました。これは、アップロードを再試行するのに適した場所です。",errorMessage)
          break
        }
      }
    }

アップロードした画像を表示する
アップロードが成功したら、StorageReference.downloadURL で取得した URL を使って画像をダウンロードして表示することができます。

do {
    let data = try Data(contentsOf: url!)
    return UIImage(data: data)!
} catch let error {
    print("Error : \(error.localizedDescription)")
}

URL から 画像の Data を取得、UIImage を生成しています。このあたりは特に変わったことはしていません。

ぱむっとぱむっと

基本的なカメラ機能の実装

//
//  ViewController.swift
//  TestCamera
//

import UIKit
import AVFoundation

class ViewController: UIViewController {

    // デバイスからの入力と出力を管理するオブジェクトの作成
    var captureSession = AVCaptureSession()
    // カメラデバイスそのものを管理するオブジェクトの作成
    // メインカメラの管理オブジェクトの作成
    var mainCamera: AVCaptureDevice?
    // インカメの管理オブジェクトの作成
    var innerCamera: AVCaptureDevice?
    // 現在使用しているカメラデバイスの管理オブジェクトの作成
    var currentDevice: AVCaptureDevice?
    // キャプチャーの出力データを受け付けるオブジェクト
    var photoOutput : AVCapturePhotoOutput?
    // プレビュー表示用のレイヤ
    var cameraPreviewLayer : AVCaptureVideoPreviewLayer?
    // シャッターボタン
    @IBOutlet weak var cameraButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCaptureSession()
        setupDevice()
        setupInputOutput()
        setupPreviewLayer()
        captureSession.startRunning()
        styleCaptureButton()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // シャッターボタンが押された時のアクション
    @IBAction func cameraButton_TouchUpInside(_ sender: Any) {
        let settings = AVCapturePhotoSettings()
        // フラッシュの設定
        settings.flashMode = .auto
        // カメラの手ぶれ補正
        settings.isAutoStillImageStabilizationEnabled = true
        // 撮影された画像をdelegateメソッドで処理
        self.photoOutput?.capturePhoto(with: settings, delegate: self as! AVCapturePhotoCaptureDelegate)
    }

}

//MARK: AVCapturePhotoCaptureDelegateデリゲートメソッド
extension ViewController: AVCapturePhotoCaptureDelegate{
    // 撮影した画像データが生成されたときに呼び出されるデリゲートメソッド
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            // Data型をUIImageオブジェクトに変換
            let uiImage = UIImage(data: imageData)
            // 写真ライブラリに画像を保存
            UIImageWriteToSavedPhotosAlbum(uiImage!, nil,nil,nil)
        }
    }
}

//MARK: カメラ設定メソッド
extension ViewController{
    // カメラの画質の設定
    func setupCaptureSession() {
        captureSession.sessionPreset = AVCaptureSession.Preset.photo
    }

    // デバイスの設定
    func setupDevice() {
        // カメラデバイスのプロパティ設定
        let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
        // プロパティの条件を満たしたカメラデバイスの取得
        let devices = deviceDiscoverySession.devices

        for device in devices {
            if device.position == AVCaptureDevice.Position.back {
                mainCamera = device
            } else if device.position == AVCaptureDevice.Position.front {
                innerCamera = device
            }
        }
        // 起動時のカメラを設定
        currentDevice = mainCamera
    }

    // 入出力データの設定
    func setupInputOutput() {
        do {
            // 指定したデバイスを使用するために入力を初期化
            let captureDeviceInput = try AVCaptureDeviceInput(device: currentDevice!)
            // 指定した入力をセッションに追加
            captureSession.addInput(captureDeviceInput)
            // 出力データを受け取るオブジェクトの作成
            photoOutput = AVCapturePhotoOutput()
            // 出力ファイルのフォーマットを指定
            photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil)
            captureSession.addOutput(photoOutput!)
        } catch {
            print(error)
        }
    }

    // カメラのプレビューを表示するレイヤの設定
    func setupPreviewLayer() {
        // 指定したAVCaptureSessionでプレビューレイヤを初期化
        self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        // プレビューレイヤが、カメラのキャプチャーを縦横比を維持した状態で、表示するように設定
        self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
        // プレビューレイヤの表示の向きを設定
        self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait

        self.cameraPreviewLayer?.frame = view.frame
        self.view.layer.insertSublayer(self.cameraPreviewLayer!, at: 0)
    }

    // ボタンのスタイルを設定
    func styleCaptureButton() {
        cameraButton.layer.borderColor = UIColor.white.cgColor
        cameraButton.layer.borderWidth = 5
        cameraButton.clipsToBounds = true
        cameraButton.layer.cornerRadius = min(cameraButton.frame.width, cameraButton.frame.height) / 2
    }
}

ぱむっとぱむっと

gurad let と if let の使い分け (nilチェック)

guard let

これ以上処理を進めたくない場合に使用します。
nilが入っていたらエラーとして扱うケースだった場合などによく使います。

let hoge: String? = nil //`hoge`の中身は`nil`

guard let fuga = hoge else { return } //`hoge`は`nil`なのでreturnされる
//`hoge`が`nil`ではなかった時の後続処理…

if let

nilだった場合に行う処理が異なる時に使用します。
nilの場合はAの処理、nilでない場合はBの処理をしたいなどのケースでよく使います。

let hoge: String? = nil //`hoge`の中身は`nil`
if let fuga = hoge { 
    //`hoge`が`nil`ではない場合にこっち
    //後続処理へ…
} else {
    //`hoge`が`nil`の場合にこっち
    //後続処理へ…
}
ぱむっとぱむっと

iPhone実機のみでBuildエラーが発生したとき。

Showing Recent Messages
The operation couldn’t be completed. Unable to log in with account . The login details for account '_______@gmail.com' were rejected.

XcodeのPreferenceのAccountを確認してログインエラーが発生していないことを確認する。

ぱむっとぱむっと

変数の有効範囲

func内で使用した変数の値を別のfuncで使用するにはViewController内で
var 変数名 : 型名?(オプショナル)で宣言して値を保持できるようにすれば使用できる。

そのViewControllerのインスタンスが開放されるまではその変数が使用できるはず。

ぱむっとぱむっと

メソッドの処理時間を計測する

ViewControllerにで確認するときには、関数呼び出しの時にself.とメソッドに付加する!
例は、PlayGroundで実行する例。

結果は小数点以下がたくさん表示されるのでラウンドしようとしたが
round関数を使用すると、整数部分のみになってしまう。

よくわからないので保留。

// 計測されるメソッド
func test() -> Void {
    for i in 0..<10000 {
        print(i)
    }
}

// 計測するメソッド
func funcTime(_ log: String, action: () -> Void) {
    let startDate = Date()
    action()
    let endDate = Date()
    print("\(log) \(endDate.timeIntervalSince(startDate))")
}


funcTime("self.test", action: {
            test()
        })

四捨五入を下記のように確認したが整数だけにしかならなかった。

// 計測されるメソッド
func test() -> Void {
    for i in 0..<1000 {
        print(i)
    }
}

// 計測するメソッド
func funcTime(_ log: String, action: () -> Void) {
    let startDate = Date()
    action()
    let endDate = Date()
//  let num : Float = 0.7922459840774536
  
//  let roundNum = round((num) * 100) / 100
  
  let realRoundNum = round(endDate.timeIntervalSince(startDate) * 100 / 100)
  
  print("結果",realRoundNum)
//  print("\(log)  \(round(endDate.timeIntervalSince(startDate)) * 100 / 100)")
//  print("\(log)  \(endDate.timeIntervalSince(startDate))")
}


// 
funcTime("self.test", action: {
            test() // ←計測される関数
        })
// 少数第3まで表示するはずだか・・・結果 1.0
ぱむっとぱむっと

クロージャー(まだまだ自由に使えない)

超ざっくりとしたクロージャの説明ですが、基本的には名前のない関数みたいなもんです。

引数と返り値を持つことができます。

例えば簡単なクロージャだと

let closure = { () -> () in
    print("クロージャ内")
}
closure() //これを実行して初めてクロージャ内のprintが行われる

() -> ()の部分は (引数) -> (返り値)を指定します。

上記の例に引数を設定してみると、

let closure = { (str:String) -> () in
    print("クロージャ内:\(str)")
}
closure("にゃーん")  //クロージャ内:にゃーん と表示
closure("わん")     //クロージャ内:わん と表示

返り値のみを設定すると、

let closure = { () -> (String) in
    let str = "もちもち"
    return str
}
print(closure())  //もちもち と表示

引数と返り値両方を設定すると

  • クロージャーのインスタンス「closure」の引数に"もちこ"を渡すと、return文のStringが返される。
  • 当たり前だけど引数、戻り値のみの場合と比べると、引数に応じて処理結果を変化させることが出来る。
let closure = { (name:String) -> (String) in
    return "ペットの名前は" + name + "です。"
}
print(closure("もちこ"))    //ペットの名前はもちこです。 と表示




ぱむっとぱむっと

UIDatePickerで日付をラベルに反映する。

DatePickerにToolItemという機能があるっぽいんので、
スタイルコンパクトでOKボタンを追加する方法を探す!
https://qiita.com/iritec/items/f05c79590640e6ebbd85

上記ができなければ、RXSwiftを入れて

// ラベルをIBに接続
 @IBOutlet weak var DateLabel: UILabel!
  
// UIDatePickerを接続
  @IBOutlet weak var datePicker: UIDatePicker!
  
  var dateString:String = ""

  override func viewDidLayoutSubviews() {
    // datePicker を透明にする
    datePicker.subviews.forEach({ $0.subviews.forEach({ $0.removeFromSuperview() }) })
  }


ぱむっとぱむっと

xibファイルを手動でStorybouadに追加する

1. xibファイルの作成

2. xibファイルclassファイルを作成。

  • xibファイルのFile's Ownerを設定する。

3. CustomViewClassの初期化コードの作成。

  • CustomViewClassにはXibで作った自作のCustomViewの初期化処理を書かないとロードされません。
import UIKit

class CustomView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadNib()
        //fatalErrorがデフォルトで入っていますが消さないとエラーになってしまうので注意してください!
    }
    func loadNib() {
        //CustomViewの部分は各自作成したXibの名前に書き換えてください
        let view = Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
    }
}

4. StoryBouadにUIViewを追加

  • 追加したUIViewにCustomViewClassを設定
ぱむっとぱむっと

カメラキャプチャセッションを停止するには

func stopCameraSession() {
    if captureSession.isRunning {
            self.captureSession.stopRunning()
    }
}
ぱむっとぱむっと

let resultview = Bundle.main.loadNibNamed("ResultView", owner: self, options: nil)?.first as! UIView
    resultview.frame = self.bounds
    datePicker.preferredDatePickerStyle = .inline
    datePicker.datePickerMode = UIDatePicker.Mode.date
    datePicker.locale = Locale(identifier: "ja")
    observeDateFeild.inputView = datePicker
    // 決定バーの生成
    let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: resultview.frame.size.width, height: 35))
    let spacelItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
    toolbar.setItems([spacelItem, doneItem], animated: true)

    // インプットビュー設定(紐づいているUITextfieldへ代入)
    observeDateFeild.inputView = datePicker
    observeDateFeild.inputAccessoryView = toolbar


 private func setupPickerView() {
     pickerView.delegate = self
     pickerView.dataSource = self
     let toolbar = UIToolbar()
     let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
     let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(ResultView.tappedDone))
     toolbar.items = [space, doneButton]
     toolbar.sizeToFit()
   itemNumTextFeild.inputView = pickerView
   itemNumTextFeild.inputAccessoryView = toolbar
 }

 @objc func tappedDone() {
   itemNumTextFeild.resignFirstResponder()
 }
ぱむっとぱむっと

extenstionで変数宣言したらExtensions may not contain stored properties

説明
Swiftでextensionするときvar宣言するとExtensions may not contain stored propertiesとエラーが出ます。stored propertiesはextensionには宣言できません。つまり、値がある変数は定義できないんです。値のある変数を定義したければ、structを使いましょう。

別ファイルにextensionを定義する

Extensions.swift
//
//  Extensions with stored properties.swift
//  Extensions with stored properties
//
//  Created by ryosuke-hujisawa on 2017/11/02.
//  Copyright © 2017年 ryosuke-hujisawa. All rights reserved.
//

import Foundation

extension ViewController {

    struct extensionStruct {
        //structの中であればvar定義できる
        var extensionVar = "extensionVarString"
    }
}
ViewControllerから呼び出す
ViewController.swift
//
//  ViewController.swift
//  Extensions with stored properties
//
//  Created by ryosuke-hujisawa on 2017/11/02.
//  Copyright © 2017年 ryosuke-hujisawa. All rights reserved.
//
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let instanceStruct = extensionStruct()
        print(instanceStruct.extensionVar)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
ぱむっとぱむっと

アンラップ方法

var resultTitleString : String?はオプショナル型の変数として宣言しているため
使用するときには、アンラップが必要です。

アンラップしないで使用するとnilが代入される。

通常はif letでアンラップするようだが、宣言した定数letを他で使用しないときは、
下記のように使用できる。

Xcodeがwarningを出して補完してくれます。


var resultTitleString : String?

   if resultTitleString != nil {
      resultTitleText.text = resultTitleString
      print("resultTitleString:",resultTitleString as Any)
    }
ぱむっとぱむっと

ClassとClassのデータの受け渡しの考え方

Class間での値の受け渡しする場合、受け渡し元と受け渡し先の両方が確実にインスタンスにアクセスできるメソッド内でデータの受け渡しをする必要がある。

そこで、受け渡しのとりがーになるイベントを発生させることができなければ、デリゲートメソッドを使うことになる。

ぱむっとぱむっと

Swiftでxibで作成したカスタムビューのインスタンスを簡単に返す

func InstantiateCustomView<T: UIView>(classToCreate: AnyClass) -> T {
    let view = UINib(nibName: NSStringFromClass(classToCreate), bundle: nil).instantiateWithOwner(nil, options: nil)[0] as T
    
    return view
}

使用する時

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let view: CustomView = InstantiateCustomView(CustomView)
        self.view.addSubview(view)
    }

}
ぱむっとぱむっと

KeyWindowの取得非推奨の対策コード

'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes

意味はよくわからないけど

let keyWindow = UIApplication.shared.connectedScenes
        .filter({$0.activationState == .foregroundActive})
        .map({$0 as? UIWindowScene})
        .compactMap({$0})
        .first?.windows
        .filter({$0.isKeyWindow}).first

他のシンプルな書き方①

let keyWindow = UIApplication.shared.windows.filter{$0.isKeyWindow}.first
let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow }
ぱむっとぱむっと

FireStoreはURL型は保存出来ないので、absoluteStringという形式でString型にキャストする必要がある。

キャストするには、下記のようにする必要があるみたい。
downloadURLがnilであれば””から文字が入るそうだ。

let url = downloadURL?.absoluteString ?? ""
ぱむっとぱむっと

FireStoreからSwiftへ日時を受け取る

SwiftとFirebaseを連携するときに、日時データを受け取りたい場合。

Firestoreの日付型はTimestampという型で格納されていますが、SwiftではDate型で利用することが多いと思うので、Timestamp->Dateへ変換までの手順を記載します。

バージョン

Swift 5
Firebase/Firestore (6.25.0)
直接Firestoreから取得する場合
TimestampクラスにはDate変換用の関数が用意されているので、それを利用するだけで済みます。


let docData = documentSnapshot.data()
let timestamp = docData["createdAt"] as! Timestamp
let date = timestamp.dateValue()

Cloud Functions経由で日時を受け取りたい場合

日付型を一度文字列にパースする必要があります。

Functions側では一度Dateに変換してからISO形式の文字列に再度変換してレスポンスデータに持たせています。

Swift側ではDateFormatterで文字列を日付に再変換してDate型として持ちます。


let dateFormatter = DateFormatter()
dateFormatter.locale = .init(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let date = dateFormatter.date(from: dateStr)
ぱむっとぱむっと

構造体の値の変更と読み出しの制限について

Animal 構造体に、nameJP、nameEN、imageName の 3 つの文字列のプロパティを定義しています。

private(set) public として定義しているのは、値を読むのはどこからでも読めますが、値をセットするのはイニシャライザー init を使ってしかできないようにする為です。

struct Animal {
    private(set) public var nameJP : String
    private(set) public var nameEN : String
    private(set) public var imageName : String
    
    init(nameJP: String, nameEN: String, imageName: String) {
        self.nameJP = nameJP
        self.nameEN = nameEN
        self.imageName = imageName
    }
}
ぱむっとぱむっと

guard let文の複数値 チェック

    guard
        let _profile = profile,
        let _credential = credential,
        let _accessToken = _credential.accessToken
    else {
// nilの場合の処理
        print("Invalid Repsonse")
        return
    }
print("nilではありませんでした。")
// nilでなければ、ここが処理される。

ぱむっとぱむっと

iPhoneアプリ開発メモ - TableViewCellのスワイプ処理

目標
スワイプしたら削除されるテーブルを作る。

準備
TableViewに最低限の設定をしておく。

Main.storyboardを次のようにする。

ViewController.swiftの内容を以下のようにする。

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    var items = ["Item1", "Item2", "Item3", "Item4", "Item5"]
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        tableView.dataSource = self
        tableView.delegate = self
    }
    
    
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "testCell")!
        cell.textLabel?.text = items[indexPath.row]
        return cell
    }
   
}

スワイプしたらボタンが出る処理
次のメソッドを実装する。

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let destructiveAction = UIContextualAction(style: .destructive, title: "") { (action, view, completionHandler) in
            self.items.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            completionHandler(true)
        }
        destructiveAction.backgroundColor = .systemPink
        destructiveAction.image = UIImage(systemName: "trash.fill")
        let configuration = UISwipeActionsConfiguration(actions: [destructiveAction])
        return configuration
    }

説明

func tableView(_,trailingSwipeActionsConfigurationForRowAt)によって、スワイプ時のボタンの設定を行える。trailingSwipeActionsConfigurationForRowAtの部分をleadingSwipeActionsConfigurationForRowAtにすると、右スワイプに対応できる。

スワイプすると出てくるボタン自体はUIContextualActionで設定する。コンストラクタ引数は以下の通り。

style: .normalか.destructiveが選択可能。.destructiveにすると、セル削除時のアニメーションがいい感じになる。
title: Actionに表示される文字を設定する。
completion: Actionが実行されたときの処理を書く。引数は(action, view, completionHandler)を持つ。
action: Action自身
view: 実行されたView
completionHandler: 実行に成功した時はcompletionHandler(true)、失敗した時はcompletionHandler(false)とする。ドキュメントにはそれしか書いておらず、何のためにそうしなければならないのかわからない。
例えば次のプロパティが設定できる。

image: ボタンの画像を設定。
backgroundColor: ボタンの背景色を設定。
最後に、UISwipeActionsConfigurationのコンストラクタにUIContextualActionを設定して、それをreturnで返す。

複数のアクションを設定することも可能で、例えば以下のように書く。

let configuration = UISwipeActionsConfiguration(actions: [destructiveAction, normalAction01, normalAction02])
フルスワイプしたら処理
実はもう実装は終わっている。もともと、フルスワイプによってActionが実行されるように作られている。UISwipeActionsConfigurationのコンストラクタで設定したactionsのうち、先頭のものが実行される仕様になっている。

もしフルスワイプをオフにしたいなら、configurationを次のようにいじる。

configuration.performsFirstActionWithFullSwipe = false
スワイプの程度によって処理を実行
例えばGmailでは、セルの1/4くらい以上スワイプしたときだけアクションが実行されるように作られている。これを実現する方法はないか。

調べ方が悪いのか、解決方法が出てこない。TableViewの標準機能でどうにかなる問題では無さそう。

方法
結局、次の方法を考えることにした。タップ処理を監視して、セルの動きを手作りする。

UIPanGestureRecognizerを各セルに設定して、どのセルの上でどれだけスライドされたかを監視する。
セルをViewで覆う(これをoverlayと呼ぶことにする)。この上に適当なコンテンツを配置する。
UIPanGestureRecognizerによって、overlayを左右に動かす。
セル自身の背景色をピンク色にする。
セルの上にゴミ箱アイコンを乗せる。
overlayが1/4以上動かされた時、ゴミ箱アイコンを少し大きくする。
overlayが1/4以上動かされた状態でタップが離されたとき、行を削除する。
今までやってきた方法と全く異なるため、別プロジェクトで作業する。

Main.storyboard
TableViewだけ配置する。TableViewCellはコードで作るため、ここでは配置しない(部品の配置やサイズの設定が難しかったため)。

TaskCell.swift
先に作っておく。AutoLayoutを一切使っていないのは、ゴミ箱を表示するためのImageViewのサイズを可変にしたいため。ゴミ箱以外については別にAutoLayoutでもよかったかもしれないが、統一性に欠けるので結局使わないことにした。

class TestCell: UITableViewCell {

    let trashImage: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "trash")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.tintColor = .white
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    let overlay: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .white
        return view
    }()
    let label: UILabel = {
        let label = UILabel()
        label.text = ""
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.backgroundColor = .systemPink
        
        overlay.addSubview(label)
        self.addSubview(trashImage)
        self.addSubview(overlay)
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didAddSubview(_ subview: UIView) {
        super.didAddSubview(subview)
        overlay.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
        label.frame = CGRect(x: 20, y: 0, width: overlay.frame.width-20, height: overlay.frame.height)
        trashImage.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
        updateTrashSize(20)
    }
    
    func updateTrashSize(_ s: CGFloat) {
        trashImage.frame = CGRect(x: 0, y: 0, width: s, height: s)
        trashImage.layer.position.x = self.frame.width - s
        trashImage.layer.position.y = 40
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code

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

          // Configure the view for the selected state
      }
}

ViewController.swift
次の点に注目。

コード上でTableCellを登録するときはtableView.registerを利用する。
Gestureを監視するためにはview.addGestureRecognizerを利用する。
UIPanGestureRecognizerは位置(location)だけでなく、どれだけ動かしたか(translation)、という情報が得られる。タップ状態はstateプロパティで調べる。
panChangedとpanEndedメソッドがごちゃごちゃしている。リファクタリングの余地があるかも。

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    private var items = ["items-1", "items-2", "items-3", "items-4", "items-5"]
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        tableView.register(TestCell.self, forCellReuseIdentifier: "testCell")
        tableView.dataSource = self
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "testCell") as! TestCell
        cell.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(cellPanned(_:))))
        cell.label.text = items[indexPath.row]
        return cell
    }
    
    
    @objc func cellPanned(_ sender: UIPanGestureRecognizer) {
        switch sender.state {
        case .began:
            panBegan(sender)
        case .changed:
            panChanged(sender)
        case .ended:
            panEnded(sender)
        default: break
        }
    }
    
    func panBegan(_ sender: UIPanGestureRecognizer) {
        // 必要なら何か処理を書く
    }
    func panChanged(_ sender: UIPanGestureRecognizer) {
        let cell = sender.view as! TestCell
        let x = cell.layer.position.x
        let dx = sender.translation(in: self.view).x
        if dx < 0 {
            cell.overlay.layer.position.x = x + dx
        }
        if abs(dx) > cell.frame.width / 4 {
            UIView.animate(withDuration: 0.2,
                           delay: 0,
                           options: .curveEaseOut,
                           animations: {
                            cell.updateTrashSize(50)
            }, completion: nil)
        }
    }
    func panEnded(_ sender: UIPanGestureRecognizer) {
        let cell = sender.view as! TestCell
        let dx = sender.translation(in: self.view).x
        if abs(dx) > cell.frame.width / 4 {
            UIView.animate(withDuration: 0.2,
                           delay: 0,
                           options: .curveEaseOut,
                           animations: {
                            cell.overlay.layer.position.x = -cell.overlay.frame.width
            }, completion: nil)
            let indexPath = self.tableView.indexPath(for: cell)!
            self.items.remove(at: indexPath.row)
            self.tableView.deleteRows(at: [indexPath], with: .none)
        } else {
            UIView.animate(withDuration: 0.2,
                           delay: 0,
                           options: .curveEaseOut,
                           animations: {
                            cell.overlay.layer.position.x = cell.layer.position.x
                            cell.updateTrashSize(10)
            }, completion: nil)
        }
    }
}

動きがいまいち自然でないが、一応それっぽいものはできた。

参考文献
UIContextualAction - Apple Developer Documenation
iOS 11でUITableViewDelegateに追加されたメソッドを使ってスワイプアクションを実装する - Developpers.IO

ぱむっとぱむっと

FireStoreとTableViewCellの削除

Swiftで子育てアプリを想定しています。
FirebaseCloud Firestoreを使用してSwiftアプリのデータを保存しています。
アプリの一部は、親が子供を追加してアプリに表示できる場所です。

テーブルビューに新しい子を追加するときに使用する子モデルを作成しました。

子モデル

protocol DocumentSerializable {
    init?(dictionary:[String:Any])
}

// Child Struct
struct Child {

    var name: String
    var age: Int
    var timestamp: Date
    var imageURL: String

    var dictionary:[String:Any] {
        return [
            "name":name,
            "age":age,
            "timestamp":timestamp,
            "imageURL":imageURL
        ]
    }
}

//Child Extension
extension Child : DocumentSerializable {
    init?(dictionary: [String : Any]) {
        guard let  name = dictionary["name"] as? String,
            let age = dictionary["age"] as? Int,
            let  imageURL = dictionary["imageURL"] as? String,
            let timestamp = dictionary["timestamp"] as? Date else {
                return nil
        }
        self.init(name: name, age: age, timestamp: timestamp, imageURL: imageURL)
    }
}

ビューで関数を実行して、アプリのテーブルビューにデータを追加しました、loadData()

私はこれの上に最初に2つの変数を設定しました:

//firestore connection
var db:Firestore!

//child array
var childArray = [Child]()

viewDidLoad

override func viewDidLoad() {
    super.viewDidLoad()

    //Connect to database
    db = Firestore.firestore()

    // call load data function
    loadData()
    checkForUpdates()

}

loadData()関数は、ログインしているユーザーデータに接続し、
そのユーザーの「子」ドキュメントを取得し、
Child Objectプロトコルを使用して子をchildArrayに追加します。

func loadData() {
        // create ref to generate a document ID
        let user = Auth.auth().currentUser
        db.collection("users").document((user?.uid)!).collection("children").getDocuments() {
            QuerySnapshot, error in
            if let error = error {
                print("\(error.localizedDescription)")
            } else {
                // get all children into an array
                self.childArray = QuerySnapshot!.documents.flatMap({Child(dictionary: $0.data())})
                DispatchQueue.main.async {
                    self.childrenTableView.reloadData()
                }
            }
        }
    }

次のようにchildArrayカウントを返すことにより、numberOfRowsInSectionを取得します。

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return childArray.count
    }

カスタムセルクラスを使用し、次のようにchildArrayコンテンツを使用してcellForRowを設定します。

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let cell = childrenTableView.dequeueReusableCell(withIdentifier: "Cell") as! ChildCellTableViewCell
        let child = childArray[indexPath.row]

        cell.childNameLabel.text = "\(child.name)"
        cell.childAgeLabel.text =  "Age: \(child.age)"

        let url = URL(string: "\(child.imageURL)")
        cell.childImage.kf.setImage(with: url)

        cell.childNameLabel.textColor = UIColor.white
        cell.childAgeLabel.textColor = UIColor.white
        cell.backgroundColor = UIColor.clear

        return cell
    }

スワイプして各テーブル行のセルを削除できるようにしたいので、次のように実装しました。

 func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if (editingStyle == UITableViewCellEditingStyle.delete) {

            childArray.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)

        }
    }

これにより、アプリから行が正常に削除されます。
しかし、私の問題は、削除された行を取得して、
Firestoreデータベースから一致するデータを削除する方法がわからないことです。

さらに複雑になるのは、各子がFirebase Storageに保存される画像を持っているので、
どういうわけかこの画像も削除する必要があることです。
画像URLは、imageURLの下の子ドキュメントに格納されています。

これについては、何らかのガイダンスまたは正しい方向を示していただければ幸いです。
FirestoreとUITableViewsに関するドキュメントがあまり見つからないので、
次に何を試してもいいのかわかりません。

更新

「canEditRow」関数では、削除したテーブル行から
Firebaseストレージから子画像を削除できましたが、
Firestoreから子ドキュメントを削除するのに苦労しています。

削除する必要があるドキュメントをクエリできますが、
このクエリからdelete()関数を実行する方法がわかりませんか?

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if (editingStyle == UITableViewCellEditingStyle.delete) {

            // 1. First Delete the childs image from storage
            let storage = Storage.storage()
            let childsImageURL = childArray[indexPath.row].imageURL
            let storageRef = storage.reference(forURL: childsImageURL)

            storageRef.delete { error in
                if let error = error {
                    print(error.localizedDescription)
                } else {
                    print("File deleted successfully")
                }
            }


            // 2. Now Delete the Child from the database
            let name = childArray[indexPath.row].name

            let user = Auth.auth().currentUser
            let query = db.collection("users").document((user?.uid)!).collection("children").whereField("name", isEqualTo: name)

            print(query)


            childArray.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)

        }
    }

答え

私はなんとかこれを解決できたと思います、そしてそれはうまくいっているようです。
「canEditRow」関数の2番目で、特定の子を見つけて(テーブルセルをスワイプして削除するときに)、Firebase Firestoreデータベースから同じ子を削除します。

これが正しい方法であるかどうか、またはエラーチェックがないかどうかはわかりませんが、
すべて機能しているようです。

誰かがここでエラーを見つけることができる場合はお知らせください、
私は本当にそれが安全に使用でき、すべてのフォールバックが設定されていることを確認したいと思います。

だから、これが私がすべてを機能させるために私がしたことです。

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if (editingStyle == UITableViewCellEditingStyle.delete) {

            // 1. First Delete the childs image from storage
            let storage = Storage.storage()
            let childsImageURL = childArray[indexPath.row].imageURL
            let storageRef = storage.reference(forURL: childsImageURL)

            storageRef.delete { error in
                if let error = error {
                    print(error.localizedDescription)
                } else {
                    print("File deleted successfully")
                }
            }

            // 2. Now Delete the Child from the database
            let name = childArray[indexPath.row].name
            let user = Auth.auth().currentUser
            let collectionReference = db.collection("users").document((user?.uid)!).collection("children")
            let query : Query = collectionReference.whereField("name", isEqualTo: name)
            query.getDocuments(completion: { (snapshot, error) in
                if let error = error {
                    print(error.localizedDescription)
                } else {
                    for document in snapshot!.documents {
                        //print("\(document.documentID) => \(document.data())")
                        self.db.collection("users").document((user?.uid)!).collection("children").document("\(document.documentID)").delete()
                }
            }})

            // 3. Now remove from TableView
            childArray.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)

        }
    }
ぱむっとぱむっと

ローカル通知


class ViewController: UIViewController {
  
  var request:UNNotificationRequest!

  @IBAction func setButton(_ sender: Any) {
    // 通知リクエストの登録
    let center = UNUserNotificationCenter.current()
    center.add(request)
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // 直接日時を設定
    let triggerDate = DateComponents(month:6, day:12, hour:16, minute:36, second: 00)
    let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false)
    
    // 通知コンテンツの作成
    let content = UNMutableNotificationContent()
    content.title = "カレンダー通知"
    content.body = "2021/6/12 16:04:00"
    content.sound = UNNotificationSound.default
    
    // 通知リクエストの作成
    request = UNNotificationRequest.init(
      identifier: "カレンダーCalendarNotification",
      content: content,
      trigger: trigger)
   
  }
}
ぱむっとぱむっと

構造体の利用の方法

struct Box {
    let width:Int
    let height:Int
    let size:String


    init(width:Int, height:Int){
        self.width = width
        self.height = height

        var sumSize = width+height

        switch (sumSize) {
        case 0...100:
            size = "S"
        case 101...200:
            size = "M"
        case 201...300:
            size = "L"
        default:
            size = "Free"
        }
    }
}

// Bozをインスタンス化(initメソッドを呼ぶ)
var newBox = Box(width: 100, height: 30)
println(newBox.size) // M

ぱむっとぱむっと

子ViewControllerから親ViewControllerのメソッドを実行する

// 親のViewController型のインスタンスを作成
let VC = self.parent as! ViewController

ViewDidLoadの後に処理を書かないと駄目みたい。

as! ViewControllerは、取得するViewControllerの名前。

ぱむっとぱむっと

画像(UIImage)を指定の範囲でトリミングする方法

切り抜きたい位置とサイズをCGRectで指定して切り抜くことができます。

func trimmingImage(_ image: UIImage, trimmingArea: CGRect) -> UIImage {
    let imgRef = image.cgImage?.cropping(to: trimmingArea)
    let trimImage = UIImage(cgImage: imgRef!, scale: image.scale, orientation: image.imageOrientation)
    return trimImage
}
ぱむっとぱむっと

SwiftでDateとStringの相互変換

import UIKit

class DateUtils {
    class func dateFromString(string: String, format: String) -> Date {
        let formatter: DateFormatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.dateFormat = format
        return formatter.date(from: string)!
    }

    class func stringFromDate(date: Date, format: String) -> String {
        let formatter: DateFormatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.dateFormat = format
        return formatter.string(from: date)
    }
}

// 元の日付の文字列
let dateString = "2015/03/04 12:34:56 +09:00"

// Dateに変換
let date = DateUtils.dateFromString(dateString, format: "yyyy/MM/dd HH:mm:ss Z")
print(date)
// => "2015-03-04 03:34:56 +0000\n"

// Stringに再変換
print(DateUtils.stringFromDate(date, format: "yyyy年MM月dd日 HH時mm分ss秒 Z"))
// => "2015年03月04日 12時34分56秒 +0900\n"

ぱむっとぱむっと

AVCaptureSessionPresetPhotoの設定を使用してキャプチャされる画像解像度を書き出す方法

以下の関数を使用すると、プログラムでCADE-0から解像度を取得できます|入力と出力を追加する前ではなく、キャプチャが始まる前:

private func getCaptureResolution() -> CGSize {
    // Define default resolution
    var resolution=CGSize(width: 0, height: 0)

    // Get cur video device
    let curVideoDevice=useBackCamera ? backCameraDevice : frontCameraDevice

    // Set if video portrait orientation
    let portraitOrientation=orientation==.Portrait || orientation==.PortraitUpsideDown

    // Get video dimensions
    if let formatDescription=curVideoDevice?.activeFormat.formatDescription {
        let dimensions=CMVideoFormatDescriptionGetDimensions(formatDescription)
        resolution=CGSize(width: CGFloat(dimensions.width), height: CGFloat(dimensions.height))
        if portraitOrientation {
            resolution=CGSize(width: resolution.height, height: resolution.width)
        }
    }

    // Return resolution
    return resolution
}
ぱむっとぱむっと

Xcodeでの実機転送をワイヤレスで行う方法。

  1. iPhoneをwifiに繋ぐ(実機転送を行うMacと同じLanを使用する)。
  2. macとiPhoneをいつも通りLightningケーブルでつなぐ。
  3. XcodeからWindow > Devices and Simulatorsを選択。
  4. Devicesタブを選択するとつないでるiPhoneが表示されるので、
    Connect via networkの項目にチェックを入れる。
  5. ケーブル抜いていつも通り実機転送する。
ぱむっとぱむっと

NotificationCenter使い方

通知名前空間の拡張

まず、下記をどこかのファイルに記載してください。
拡張のため、どのファイルに書かないといけないみたいなことはありませんが、
あとから見返してわかる場所に書くことがベストだと思います。

extensions.swift
extension Notification.Name {
    static let notifyName = Notification.Name("notifyName")
}

notificationCenter.post(name: .notifyName, object: nil)name: .notifyNameで通知を送るためのトリガーをしている!

SendViewController.swift
// 通知を送りたい箇所でこのように記述
NotificationCenter.default.post(name: .notifyName, object: nil)

通知を受け取りたい側
まずはじめに、受け取りたい側のVCでNotificationCenterを登録します。
こうすることで、notifyName通知が発行されたタイミングで、doSomething()が実行されるようになります。

name: .notifyNameでなんの通知取りがを受け取ってメソッドを実行するか設定されている。
#selector(doSomething(notification:))

catchViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    /// NotificationCenterを登録
    NotificationCenter.default.addObserver(self, selector: #selector(doSomething(notification:)), name: .notifyName, object: nil)
}

ぱむっとぱむっと

ModalViewのDisMissは遷移先で実行しても遷移元で実行してもどちらでも実行される。

この場合はどちらに書くのも正しいです。同じ動作をします。それは意図した挙動で正しいです。

The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.

https://developer.apple.com/documentation/uikit/uiviewcontroller/1621505-dismiss

このように書いてある通り、dismiss()メソッドはモーダルビューとして表示先の子ビューコントローラを閉じます。ただし、子ビューコントローラがそれ以上モーダル表示をしていない状態でdismiss()を読んだ場合は、自身を表示した表示元の親ビューコントローラに処理を移譲するので、結果として表示元のビューコントローラからdismiss()メソッド呼ぶのと同じことになります。

子ビューコントローラ(ここではModalViewController)がさらに別のビューコントローラをモーダル表示していた場合は同じ結果にならず、その子ビューコントローラだけが閉じられます。

このとき、一番大元のビューコントローラ(ここではViewController)のdismiss()メソッドを読んだ場合はそれ以下のビュー(ModalViewControllerとその子ビューコントローラ)をすべて閉じます。

この挙動は複数のモーダルビューを一度に閉じたい場合に活用します。

まとめると、モーダル表示したビューコントローラが1枚の場合(表示元と表示先しかない場合)はどちらのdismiss()メソッドを呼んでもまったく同じ結果になります。どちらを呼ぶのも正しいです。

モーダル表示を連続して2回以上行っているときは、一番上に重なっているビューコントローラとそのすぐ下のビューコントローラについては同じ結果になります。

それ以上下のビューコントローラについては自分より上のモーダルビューをすべて閉じます。

モーダル表示したビューコントローラが1枚の場合と2枚以上の場合で分けてまとめましたが、同じことを行っているだけです。

ぱむっとぱむっと

Any型では値の比較できない

比較演算子を使うには、Equatableプロトコルに準拠している必要がある。

Comparableプロトコルは値の大小関係を確認するプロトコル。
Equatableプロトコルは同値性を検証するためのプロトコル。

ぱむっとぱむっと

TableViewのCellをxibで作成したときに、画面遷移しな意図機の対処法

情報元
https://www.millenbox2.com/entry/2020/06/04/124327
xibでcellを作成すると、『UITableViewCellタップで紐付けられていたSegueが実行されなくなった為』だそうです。

回避するにはdidSelectRowAtperformSegueを呼び出す必要があるよです。

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    performSegue(withIdentifier: "showDetail", sender: nil)  
}

ぱむっとぱむっと

DateFormatterを汎用化する

DateFormatterの生成が重いようなので、処理を効率化するために下記コードをメモしておく!

シングルトンで保持してどこからでも利用できる。


class MyDateFormatter {
    static let shared = MyDateFormatter()
    private let yyyyMMddDateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "yyyy/MM/dd"
        return dateFormatter
    }()

    private let HHmmDateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "HH:mm"
        return dateFormatter
    }()

    func formatToyyyyMMdd(from date: Date) -> String {
        return yyyyMMddDateFormatter.string(from: date)
    }

    func formatToHHmm(from date: Date) -> String {
        return HHmmDateFormatter.string(from: date)
    }
}

利用するときは下記のように記述

let dateString = MyDateFormatter.shared.formatToyyyyMMdd(from: date)
ぱむっとぱむっと

UITextFieldをコードで追加

// UITextFieldを生成
let textField = UITextField()
textField.frame = CGRect(x: 10, y: 100, width: UIScreen.main.bounds.size.width-20, height: 38)
// プレースホルダを設定
textField.placeholder = "入力してください。"
// キーボードタイプを指定
textField.keyboardType = .default
// 枠線のスタイルを設定
textField.borderStyle = .roundedRect
// 改行ボタンの種類を設定
textField.returnKeyType = .done
// テキストを全消去するボタンを表示
textField.clearButtonMode = .always
// UITextFieldを追加
self.view.addSubview(textField)
// デリゲートを指定
textField.delegate = self

UITextFieldDelegateのメソッド

// 改行ボタンを押した時の処理
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    print("Return")
    return true
}

// クリアボタンが押された時の処理
func textFieldShouldClear(_ textField: UITextField) -> Bool {
    print("Clear")
    return true
}

// テキストフィールドがフォーカスされた時の処理
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
    print("Start")
    return true
}

// テキストフィールドでの編集が終了する直前での処理
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
    print("End")
    return true
}

キーボードタイプ(keyboardType)

.asciiCapable :標準のASCII文字を表示するキーボード
.asciiCapableNumberPad :ASCII数字だけを出力する数字パッド
.decimalPad :数字と小数点のあるキーボード
.default :現在の入力メソッドのデフォルトキーボード
.emailAddress :電子メールアドレスを入力するために最適化されたキーボード
.namePhonePad :人の名前または電話番号を入力するためのキーパッド
.numberPad :テンキーパッド
.numbersAndPunctuation :数字と句読点のキーボード

枠線のスタイル(borderStyle)

.bezel :立体的な四角い枠線
.line :四角い枠線
.none :枠線なし
.roundedRect :角丸の枠線

改行キータイプ(returnKeyType)

.continue :Continue(続ける)
.default :return(改行)
.done :Done(完了)
.emergencyCall :EmergencyCall(緊急電話)
.go :Go(開く)
.google :Search(検索)
.join :Join(接続)
.next :Next(次へ)

クリアボタン表示指定(clearButtonMode)

.always :常に表示
.never :表示しない
.unlessEditing :入力されたテキストがあり、フォーカスが当たっていない時に表示
.whileEditing :入力されたテキストがあり、フォーカスが当たっている時に表示

ぱむっとぱむっと

二つ前の画面に戻る方法

self.present(resultVC, animated: true, completion: nil)で生成されたViewcontrollerをDismissする場合

2つ前のViewcontrollerのインスタンスを取得してそのインスタンスでViewcontrollerのdismissを行うということ。

    //MARK:2つのViewControllerをDismiss(NotifiRegitViewContとResultViewContをDismiss。)
    self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)

参照サイト

ぱむっとぱむっと

UIViewやButtonに枠線をつける

UIView.layer.borderColor = UIColor.greenColor().CGColor

UIView.layer.borderColor = UIColor(named: "MainTextColer")?.cgColor

button.layer.borderColor = UIColor.greenColor().CGColor

ぱむっとぱむっと

直接Labalに値を設定できない理由
インスタンス変数に値を設定するのではなく、次のように直接ラベルテキストに値を設定すれば、インスタンス変数を用意する必要が無いため、合理的なように思えます。

nextView.label1.text = textField1.text!

しかし、これは実行時にエラーになります。

遷移先のViewControllerのインスタンスを取得した時点では、 まだラベル部品のインスタンスが存在していない為です。

ぱむっとぱむっと

ライブラリMIBlurPopup設定ポイント

ViewControllerのBaseViewの色設定:Opacity

SegueにClass "MIBlurPopup"を設定

ぱむっとぱむっと

もう一つのSegueの書き方

方法1

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "toSegueViewController" {
            let nextVC = segue.destination as! SegueViewController
            nextVC.text = "fromViewController"
        }

方法2

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      super.prepare(for: segue, sender: nil)
      
      guard let questionVC = segue.destination as? QuestionViewCont else { return }
    questionVC.customBlurEffectStyle = .dark // 渡す値
  }

ぱむっとぱむっと

UserDefaultのExtension使い方

UserDefaultで使用できる型

保存時の処理 参照時の処理
配列型:[Any] set:forKey: array:forKey
真偽型:Bool set:forKey: bool:forKey
データ型: Data set:forKey: data:forKey
辞書型:[String: Any] set:forKey: dictionary:forKey
浮動小数点型: Float set:forKey: float:forKey:
整数型:Int set:forKey: integer:forKey:
オブジェクト型:Any set:forKey: object:forKey:
文字列型配列:[String] set:forKey: stringArray:forKey:
文字列型:String set:forKey: string:forKey:
倍精度浮動小数点型:Double set:forKey: double:forKey:
URL型:URL set:forKey: url:forKey:

定義したUserDefaultのExtension


extension UserDefaults {
    // 保存したいUIImage, 保存するUserDefaults, Keyを取得
    func setUIImageToData(image: UIImage, forKey: String) {
        // UIImageをData型へ変換
//  pngData型に変換するとimageOrientationデータが欠損するためjpegDataに変換すること
//        let nsdata = image.pngData()
          let nsdata = image.jpegData(compressionQuality: 1.0)
        // UserDefaultsへ保存
        self.set(nsdata, forKey: forKey)
    }
    // 参照するUserDefaults, Keyを取得, UIImageを返す
    func image(forKey: String) -> UIImage {
        // UserDefaultsからKeyを基にData型を参照
        let data = self.data(forKey: forKey)
        // UIImage型へ変換
        let returnImage = UIImage(data: data!)
        // UIImageを返す
        return returnImage!
    }

}

使い方

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!

    // UserDefaultsの宣言
    let ud = UserDefaults.standard
    // 保存するUIImage
    var hogeImage: UIImage = UIImage(named: "spiderman.jpg")!

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

        // 保存するとき
        // UserDefaultsにsetする(保存するUIImage, 宣言したUserDefaults, Keyの文字列)
        ud.setUIImageToData(image: hogeImage, forKey: "image")


        // 参照するとき
        // UIImage型の変数に参照する(宣言したUserDefaults, Keyの文字列)
        hogeImage = ud.image(forKey: "image")
        // UIImageViewに表示
        imageView.image = hogeImage
    }
}

引用元:Qiita

ぱむっとぱむっと

スクロールビュー(ScrollView)の基本的な考え方と基礎的な実装方法

基本的な考え方

  • ScrollViewはSubViewを表示する窓❗
  • ScrollViewはSubViewより小さい大きさになるように制約を設定する❗

具体的な手順

実装例(スクロールビューの中にUIViewがあり、1つのラベルがある。そして、上下のみにScrollする。)

  1. ViewControllerのSuperView(safeArea)に対してScrollViewのスペーシングを上下左右 "0"に設定する。
  2. ScrollViewのSubViewをUIView(ContentsViewとする)で作成してScrollViewに入れる。
  3. ScrollViewとContentViewの上下左右に4箇所を"0"で制約をつける。
  4. ScrollViewとContentViewを2つ選択して、EqualWidth、EqualHeightを設定する。
  5. ContentViewのPriority設定を250に設定する
  6. ContentViewのHeightに1200の制約を設定する。(ScrollViewより大きなHeightを設定する。)
ぱむっとぱむっと

日付の判定 (過去・未来・今)

  // UIDatePickerのDoneを押したら発火
  @objc func notifiDone() {
    // テンプレートとロケールを組み合わでその国に沿った表示になります。
    formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "ydMMM", options: 0, locale: Locale(identifier: "ja_JP"))
    
    
    let today = Date() + 4.days
    
    let set1 = datePicker.date + 1.minutes
    
    print("today:",today)
    print("set1:",set1)
    
    let kekka = set1.day - today.day
    
    if set1.compare(today) != .orderedDescending {
      print("過去なので入力させない")
      return
    } else {
      // FireStoreのTimeStampも時間をゼロにするため一旦Stringにキャストして時間を00にしてからDate型に再変換している。
      notifidateTextFeild.text = "\(formatter.string(from: datePicker.date))"
      
      let kekkaInt = kekka
      
      print("kekkaInt:",kekkaInt)
      print("kekka:",kekka)
      
      toDateLabel.text = "\(kekkaInt)日後"
      
      let notifiDateString = "\(formatter.string(from: datePicker.date))"
      let notifiDateOnly = DateUtils.dateFromString(string: notifiDateString, format: "yyyy年MM月dd日")
      notifiDateData = notifiDateOnly
      
      notifidateTextFeild.endEditing(true)
      
    }
    
  }
    

ComparisonResultというenumで今か未来か過去かを判定することができます。どれがどれかはコメントの記載通りです。

public enum ComparisonResult : Int {

    //未来
    case orderedAscending
    //今
    case orderedSame
    //過去
    case orderedDescending
}

使ってみる

let past = Date(timeIntervalSinceNow: -100)

let now = Date()

let future = Date(timeIntervalSinceNow: 100)

if now.compare(future)  == .orderedAscending {
    print("今は未来より過去")
}

if now.compare(now) == .orderedSame {
    print("今と今は同じ時間")
}

if now.compare(past) == .orderedDescending {
    print("今は過去より未来")
}
ぱむっとぱむっと

 case .length(Lenght.minimum, let minimum as Int):
	return { (evidence: String) -> Bool in
		return evidence.count >= minimum
	}
			
 case .length(Lenght.maximum , let maximum as Int):
	return { (evidence: String) -> Bool in
		return evidence.count <= maximum
	}
			

ぱむっとぱむっと

CDAlertView使い方

    //MARK: CDAlertViewの定義
    let firstQuestion = CDAlertView(title: "ヘタ部分のやわらかさは?", message: "メッセージ本文\n2行目\n3行目。\n\n5行目\n6行目", type: .success)
    firstQuestion.hideAnimations = { (center, transform, alpha) in
                transform = .identity
                alpha = 0
            }

Button部分の定義

    //MARK: ボタン定義
//Yesボタン定義
    let yesAction1 = CDAlertViewAction(title: "はい! 💪") { CDAlertViewAction in
  
      secondQuestion.show()
      
     return true
    }
//Noボタン定義    
    let noAction1 = CDAlertViewAction(title: "いいえ。 😑") { CDAlertViewAction in
      
      secondQuestion.show()
      return true
    }

 

Alertの呼び出し

   firstQuestion.add(action: yesAction1)
    firstQuestion.add(action: noAction1)

// メッセージフォントの設定とAlert幅の設定
    firstQuestion.messageFont = UIFont.systemFont(ofSize: 17)
    firstQuestion.popupWidth = CGFloat(300)
    
    firstQuestion.show(){ (alert) in
  }
ぱむっとぱむっと
//MARK: 購入日決定purchaDateDone()
  @objc func purchaDateDone() {
    
    // テンプレートとロケールを組み合わでその国に沿った表示になります。
    formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yMMMd", options: 0, locale: Locale(identifier: "ja_JP"))
    
    let purcharseDateString = "\(formatter.string(from: datePicker.date))"
    let purchDateOnly = DateUtils.dateFromString(string: purcharseDateString, format: "yyyy年MM月dd日")
    purchaDateData = purchDateOnly
    purchaseDateTextFeild.endEditing(true)

    let compareDate = datePicker.date + 1.minutes
    let daysBefore = compareDate.day - today.day
    purchaseDateTextFeild.text = "\(formatter.string(from: compareDate))"
    
    print("daysBefore日数差:",daysBefore)
    
    if daysBefore == 0 {
      purchaDateMessage.text = "今日"
      print("purchaDateMessage:",purchaDateMessage.text as Any)
    } else if daysBefore >= 1 {
      purchaDateMessage.text = "\(daysBefore)日後"
    } else if daysBefore < 0 {
      purchaDateMessage.text = "\(-daysBefore)日前"
    }

  }


// MARK:通知日決定purchaDateDone()
  @objc func notifiDateDone() {
    
    // テンプレートとロケールを組み合わでその国に沿った表示になります。
    formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yMMMd", options: 0, locale: Locale(identifier: "ja_JP"))

    let compareDate = datePicker.date + 1.minutes
    let daysLater = compareDate.day - today.day
    
    let notifiDateString = "\(formatter.string(from: datePicker.date))"
    let notifiDateOnly = DateUtils.dateFromString(string: notifiDateString, format: "yyyy年MM月dd日")

    print("日数差:",daysLater)
    
    if daysLater < 0 {
      notifiDateMessage.text = "過去の日付は入れれません。"
      return
      
    } else if daysLater == 0 {
      notifiDateTextFeild.text = "\(formatter.string(from: compareDate))"
      notifiDateData = notifiDateOnly
      print("notifiDateMessage:",notifiDateMessage.text as Any)
      notifiDateTextFeild.endEditing(true)
      notifiDateMessage.text = "今日"
    } else if daysLater >= 1 {
      notifiDateTextFeild.text = "\(formatter.string(from: compareDate))"
      notifiDateData = notifiDateOnly
      notifiDateTextFeild.endEditing(true)
      notifiDateMessage.text = "\(daysLater)日後"
    }
  }

ぱむっとぱむっと

Swiftハンドラとは?

一般的にはハンドラはプログラム中で関数やサブルーチンなどの形で実装され、メモリ上に展開されるが、通常のプログラムの流れには組み込まれず、普段は待機している。

そのハンドラが対応すべき処理要求が発生するとプログラムの流れを中断してハンドラが呼び出され、要求された処理を実行する。

対応付けられた事象の種類により「イベントハンドラ」「割り込みハンドラ」などの種類がある。

swiftではクロージャと同じ機能であるらしい。

ぱむっとぱむっと

@escaping属性とは?

  • コンパイラに対し、宣言や型の補足情報を伝えるもの
  • 属性や修飾子とも呼ばれる
  • escaping属性を含めて27種類ある。

@〜という記法はコンパイラディレクティブと呼ばれ、コンパイラに対する指示を記載する際に利用される

@escaping属性とは?

  • クロージャをスコープ外でも保持する必要があることを示す
  • 以下の例では、completionクロージャーの実行を非同期で実行しているため、スコープ外ではクロージャーが保持されず、コンパイルエラー
// escapingなし ->  NG: コンパイルエラー
func someAsyncMethod(completion: () -> Void) {
    DispatchQueue.main.async {
       completion()
    }
}

// escapingあり -> OK
func someAsyncMethod(completion: @escaping () -> Void) {
    DispatchQueue.main.async {
       completion()
    }
}