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

【Swift5】iOS14のUIDatePickerで日付を選択する

概要

DatePicker で日付選択をかっこよく、かつ楽に実装しようとしたところ
デザインがうまいこといかなかった際の試行錯誤を備忘録として残す。
ゴリ押し実装なので軽い気持ちでみてください。

この記事の完成品

日付のラベル部分を押すとカレンダーから日付が選べる
選択が終わるとアニメーションしながら画面が閉じてラベル部分に日付が適応される

やりたかったこと

---年-月-日と表示されている日付をタップするとiOS14のカレンダー式日付選択画面が表示される。
デザインはこんな感じ

未選択前

選択後

なんかiOS14からDatePickerの挙動がかっこよくなったのでそれを使えば簡単だろうと思っていました。

iOS14のDatePicker関連の記事はこちら
https://qiita.com/kj_trsm/items/a53b0b3f7e1bc7c06106

Try & Error

1:何も考えずUIButtonを置いてみた

ボタンを置いてデザインを合わせるも、いやいやDatePicker呼べないじゃんってなった(ColorPickerみたいに使うと思ったら違った)
カラーピッカーはこちら
https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204184/3ebb806c-59bf-6dd3-adb9-335026e7101d.jpeg

2:DatePickerを置いてみる

上に書いた記事にもあるが、Styleがいくつかある
今回のデザインに近いであろう

Hoge.swift
datePicker.preferredDatePickerStyle = .compact
datePicker.datePickerMode = .date

見た目はこんな感じ

!

挙動は希望通りなのであとはデザイン調整だ!っという気持ちになるが・・・
デザイン変更が上手いこといかない

  • 背景色のグレーはなんだ? --> datePicker.backgroundColor = .clear にするも、変化なし
  • 文字を左揃えにしたい --> UILabel みたいな設定が見つからない
  • ----年-月-日という形式で表示させるのどうやるの?

解決方法

背景色のグレーはなんだ?

Hoge.swift
datePicker.subviews[0].subviews[0].subviews[0].backgroundColor = .clear

で背景色が変えられた

文字を左揃えにしたい
-> 分からん、無理だった

文字の左揃えができなかったので、ゴリ押しでUILabelの上にUIDatePickerを重ねることに
重ねた上で

Hoge.swift
datePicker.subviews.forEach({ $0.subviews.forEach({ $0.removeFromSuperview() }) })

でdatePickerのSubViewsを消去することで謎のグレー背景も、日付の文字も消える。
重ねた下にあるUILabelの文字を、あたかもUIDatePickerの文字のように表示させる
そうすることでタップ時にはUIDatePickerが動作、表示する日付はUILabelとなり、簡単に希望通りの動作を作れた。

  • ----年-月-日という形式で表示させるのどうやるの? -> UILabelに表示させることにしたので調査してません

完成形

StoryboardでUILabelとUIDatePickerを重ねて、UILabelに---年-月-日と設定

HogeViewController.swift
import UIKit
import RxSwift
import RxCocoa

class HogeViewController: UIViewController {

    @IBOutlet var datePicker: UIDatePicker!
    @IBOutlet var dateLabel: UILabel!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        datePicker.rx.value.changed.asObservable()
            .subscribe({ event in
                if let date = event.element{
                    self.dateLabel.text = date.toDisplayString()
                }
            })
            .disposed(by: disposeBag)
    }

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

適当にdatePickerの値の変更を監視してUILabelに日付として設定させれば完成
タップしたら値が設定され、閉じる時のアニメーションも勝手にDatePickerがやってくれた

おわりに

結局重ねるというオチになってしまった。
きっともっとちゃんとした解決策がありそうだけど備忘録として残しておきます。

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

Swift初学者が電子レンジロジックを実装してみた!

概要

現在、私はMENTAサービスを利用し、ヤマタクメンターにご指導いただいています。

ヤマタクメンターやサービスについて知りたい方はこちら!
(https://menta.work/plan/584)

その中で、今回は、応用学習の第2段ということで、
「電子レンジロジックの実装」の課題を行ったので、アウトプットしていきます。

なお、今回の課題はSwift初学者が作ったロジックであり、至らぬ点が多々あると思います。どうか、温かい目で見守っていただけると幸いです。

アドバイス等ありましたら、ご教授いただけると幸いです。

環境

-Playground

実装の要件

・電子レンジのワット数は900,600,200とする
・今回は、安全のために11分以上は加熱できないものとする

・タイマーの実装
  ・分と秒の概念を実装
  ・ 残り時間の出力
  ・残り時間が0秒になったら、「温め終了です」と出力し、タイマーを停止
  ・温める時間が11分以上だと「温めを開始できません!」、
   「タイマーを10分59秒以内に設定してください」と出力

・タイマーが10分59秒以内にセットされたら、温め可能フラグを返し、「温め開始!!」を出力
・選択されたワット数を出力

実装

VirtualVendingMachine.rb
import UIKit
import PlaygroundSupport

class VirtualMicrowave : UIViewController {

    //タイマーをセットしてください
    var timeSet = (min:0,sec:0)

    //Timerクラスの初期化
    var timer: Timer!

    //ワット数を決める
    enum powerConsumptionType{

        case bottun900W
        case bottun600W
        case bottun200W


    var displayName: String{

            switch self {

            case.bottun900W :

                return "900W"

            case.bottun600W :

                 return "600W"

            case.bottun200W :

                return "200W"

            }

    }

    }


    override func viewDidLoad() {
       super.viewDidLoad()

        //スタートタイマーを呼び出す
        startTimer()

   }


   override func viewWillAppear(_ animated: Bool) {
       super.viewWillAppear(animated)
   }

   override func didReceiveMemoryWarning() {
       super.didReceiveMemoryWarning()
   }


    //温め可能フラグを返す
    func startWarm(type: powerConsumptionType) -> Bool {


       var isWarmable = false


        if  timeSet.min < 11 {

            print("温め開始!!:\(type.displayName)")

           return true


        }


        return isWarmable

        //カウントダウンタイマーを呼び出す
        countDownTimer()


    }


@objc func countDownTimer(){


    if timeSet.min == 0 && timeSet.sec == 0 {

           print("温め終了です")
           timer.invalidate()


   }else if timeSet.min > 10{


        print("温めを開始できません!")
        print("タイマーを10分59秒以内に設定してください")
        timer.invalidate()



    }else if timeSet.sec > 0 && timeSet.min < 11 {

    //秒数が0以上の時または10分以下の時

        timeSet.sec -= 1
        print("\(timeSet.min)\(timeSet.sec)秒")


    } else if (timeSet.sec == 0) && timeSet.min > 0 {

    //秒数が0の時

        timeSet.sec += 59
        timeSet.min -= 1

        print("\(timeSet.min)\(timeSet.sec)秒")


    }


    }

    //タイマーを起動するメソッド
    func startTimer() {
          timer = Timer.scheduledTimer(
               timeInterval: 1,
               target: self,
            selector: #selector(self.countDownTimer),
               userInfo: nil,
               repeats: true)
       }
    }


let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 500))
let viewController = VirtualMicrowave()
viewController.view.backgroundColor = UIColor.gray
window.rootViewController = viewController
window.makeKeyAndVisible()
PlaygroundPage.current.liveView = window

let virtualMicrowave = VirtualMicrowave()
var isSuccessToWarm = virtualMicrowave.startWarm(type:.bottun200W)

print(isSuccessToWarm)

まとめ

今回は、引数の使い方やPlaygroundの環境ではあるが、タイマーメソッドの使い方を学んだ。
ゼロからロジックを考えたので、基礎構文の理解が深まった。
リファタリングを考えるとすれば、レンジ機能だけではなくオーブン、トースト機能などを追加してもいいかもしれません。

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

【Swift5】CustomViewをコードで実装する

はじめに

CustomViewをコードで実装した際に、initメソッドに関しての理解が曖昧でしたので備忘録として投稿します。

動作環境

【Xcode】Version 12.0.1
【Swift】Version 5.3

実装コード

CustomView.swift
import UIKit

class CustomView: UIView {

    var imageView: UIImageView!

    // 詳細説明①
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    // 詳細説明②
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup(){
        let iWidth: CGFloat = 200
        let iHeight: CGFloat = 200
        let posX: CGFloat = (self.frame.width - iWidth)/2
        let posY: CGFloat = (self.frame.height - iHeight)/2

        imageView = UIImageView(frame: CGRect(x: posX, y: posY, width: iWidth, height: iHeight))
        imageView.image = UIImage(named: "hoge")
        self.addSubview(imageView)
    }

}
ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let customView = CustomView()
        customView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
        self.view.addSubview(customView)
    }
}

詳細説明①

override init(frame: CGRect) {
    super.init(frame: frame)
}
  • 通常のイニシャライザ(requiredが付かないイニシャライザ)はサブクラスで実装しても実装しなくても良いが、初期化などを行う場合はoverrideを必ず付ける必要があります。

詳細説明②

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}
  • 必須のイニシャライザ(requiredが付いてるイニシャライザ)はサブクラスで絶対に実装しないといけません。
  • init?(coder aDecoder: NSCoder)のイニシャライザはNSCodingプロトコルで定義されており、それがUIViewクラスで実装されているので、サブクラスでも実装する必要があるということになります。

参考

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

CustomViewをコードで実装する

はじめに

CustomViewをコードで実装した際に、initメソッドに関しての理解が曖昧でしたので備忘録として投稿します。

動作環境

【Xcode】Version 12.0.1
【Swift】Version 5.3

実装コード

CustomView.swift
import UIKit

class CustomView: UIView {

    var imageView: UIImageView!

    // 詳細説明①
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    // 詳細説明②
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup(){
        let iWidth: CGFloat = 200
        let iHeight: CGFloat = 200
        let posX: CGFloat = (self.frame.width - iWidth)/2
        let posY: CGFloat = (self.frame.height - iHeight)/2

        imageView = UIImageView(frame: CGRect(x: posX, y: posY, width: iWidth, height: iHeight))
        imageView.image = UIImage(named: "hoge")
        self.addSubview(imageView)
    }

}
ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let customView = CustomView()
        customView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
        self.view.addSubview(customView)
    }
}

詳細説明①

override init(frame: CGRect) {
    super.init(frame: frame)
}
  • 通常のイニシャライザ(requiredが付かないイニシャライザ)はサブクラスで実装しても実装しなくても良いが、初期化などを行う場合はoverrideを必ず付ける必要があります。

詳細説明②

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}
  • 必須のイニシャライザ(requiredが付いてるイニシャライザ)はサブクラスで絶対に実装しないといけません。
  • init?(coder aDecoder: NSCoder)のイニシャライザはNSCodingプロトコルで定義されており、それがUIViewクラスで実装されているので、サブクラスでも実装する必要があるということになります。

参考

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

CIFilterのblurぼかしで画像を小さく縮ませない方法

画像が縮む問題

CIFilterのCIGaussianBlurでCIImageをぼかすと、画像がShrinkしてしまう。

[画像] 左:オリジナル 右:ぼかし
オリジナル.png チヂミ.png

let blurredImage = CIFilter(name: "CIGaussianBlur", parameters: [kCIInputImageKey:originalCIImage,
                                                                 kCIInputRadiusKey:50])?.outputImage?

CGImageを操作したり、AccelerateやMetalPerformanceShader、OpenCVのフィルターを使うと元と同じサイズのぼかし画像が得られるけれど、メモリ使用が大きい。
CoreImageのなかで完結すると、何枚ぼかしても数MBしかメモリを使わない。

方法

let cropSize = CGRect(x: 0, y: 0, width: originalCIImage.extent.width * 0.999, height: originalCIImage.extent.height * 0.999)
let blured = CIFilter(name: "CIGaussianBlur", parameters: [kCIInputImageKey:originalCIImage,
                                                           kCIInputRadiusKey:50])?.outputImage?.cropped(to: cropSize).resize(as: originalCIImage.extent.size)

[画像]調整結果
調整.png

Cropすると画像サイズに合わせてくれます。

リサイズメソッドはこちら↓

extension CIImage {
    func resize(as size: CGSize) -> CIImage {
        let selfSize = extent.size
        let transform = CGAffineTransform(scaleX: size.width / selfSize.width, y: size.height / selfSize.height)
        return transformed(by: transform)
    }
}

?


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

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

Twitter
Medium

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

iOS13のPopover修正について

はじめに

今更ですが、iOS13からpopoverのmaskの重なり順が変更になっていたようで。。。
気付いたらpopoverで表示しているpickerなどが見切れていたので、
修正方法を備忘録としてまとめます。

環境

swift5
Xcode 11.4.1

修正前

UIのズレはこんな感じです。
左右に矢印がある時にはっきり見切れてしまいます。

ちなみに元々の実装はこんな感じにしてました。

hoge.swift
// ImagePicker表示
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
imagePicker.modalPresentationStyle = .popover
imagePicker.mediaTypes = [kUTTypeMovie as String]

let popover = imagePicker.popoverPresentationController
popover?.sourceView = button
popover?.sourceRect = button.bounds

present(imagePicker, animated: true, completion: nil)

修正方法

UIViewControllerのextensionを作成しました。

UIViewController+Picker.swift
extension UIViewController {

    /// Picker用のVCを作成し、Pickerを表示する(imagePicker用)
    ///
    /// - Parameters:
    ///   - imagePickerController: UIImagePickerController
    ///   - source: pickerの表示元となるView
    func presentPickerView(_ imagePickerController: UIImagePickerController, source: UIView) {
        guard let imagePickerView = imagePickerController.view else { return }
        // 土台となるVCを作成します
        let customPickerViewController = UIViewController()
        // まず先にaddChildします
        customPickerViewController.addChild(imagePickerController)
        // 土台のVCとimagePickerを同じ大きさにします
        imagePickerController.view.frame = customPickerViewController.view.frame
        // subViewになりうるviewにAutoLayoutをコード上で指定する場合はfalseにする
        imagePickerController.view.translatesAutoresizingMaskIntoConstraints = false
        // addSubviewします
        customPickerViewController.view.addSubview(imagePickerView)
        // ここで土台VCのsafaAreaに対してImagePickerの制約を指定をしていきます
        NSLayoutConstraint.activate([
            imagePickerController.view.leadingAnchor.constraint(equalTo:
                customPickerViewController.view.safeAreaLayoutGuide.leadingAnchor),
            imagePickerController.view.trailingAnchor.constraint(equalTo:
                customPickerViewController.view.safeAreaLayoutGuide.trailingAnchor),
            imagePickerController.view.topAnchor.constraint(equalTo:
                customPickerViewController.view.safeAreaLayoutGuide.topAnchor),
            imagePickerController.view.bottomAnchor.constraint(equalTo:
                customPickerViewController.view.safeAreaLayoutGuide.bottomAnchor)
        ])
        // didMoveでset完了
        imagePickerController.didMove(toParent: customPickerViewController)

        // 以下はpickerの基本的な実装に戻ります。
        // 土台のViewに大きさを指定(=pickerの大きさ)
        customPickerViewController.preferredContentSize = CGSize(width: 200,
                                                                 height: 200)
        customPickerViewController.modalPresentationStyle = .popover
        let popover = customPickerViewController.popoverPresentationController
        popover?.sourceView = source
        popover?.sourceRect = source.bounds

        present(customPickerViewController, animated: true, completion: nil)
    }
}

使う時はこんな感じ

ViewController.swift
@IBOutlet func TapButton(_ sender: UIButton) {
    // 表示したいpickerを作成
    let imagePicker = UIImagePickerController()
    // delegateとかは表示するclassで設定する
    imagePicker.delegate = self
    imagePicker.sourceType = .photoLibrary
    // extensionを使用して表示
    presentPickerView(imagePicker, source: button)
}

おわり。

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

[Swift]キーボード上にあるツールバーの高さを動的に変更する方法

はじめに

UITextFieldやUITextViewで表示されるキーボード上のツールバーに、バー上のボタンをタップするともう1段バーが表示される方法になります。
イメージとしてはiOS純正のリマインダーアプリで実装されている、リマインド時間やロケールを選択するツールバーを同じものになります。
textView.inputAccessoryView = toolbarでキーボード上にツールバーを設定する基本的な方法はよく見かけますが、
動的にバーの高さを変更する方法が見当たらなかったので参考になれば幸いです。

完成イメージ

ツールバー上のベルのボタンをタップすると、もう1段上に通知時間のインターバルを選択するボタンが表示されます。
RocketSim Recording - iPhone 12 Pro Max - 2020-11-13 16.39.07.gif

実装方法

はじめはtextView.inputAccessoryView = toolbarで設定したtoolbarの高さを動的に変更しようとしたのですが、なかなかうまくできず。。。
(実装方法わかる方いれば教えてください!)
今回はtextView.inputAccessoryView = toolbarで設定したtoolbarに対して子viewをaddSubviewsendSubviewToBackで裏側に隠しておいて、
上下に移動することで表示を切り替えています。

今回はUITableViewにCustomTableViewCellを作成し、セル上にUITextViewを配置しています。
キーボード上にツールバーを表示するためにCustomTableViewCell内でUITextViewに対してキーボードが表示されるようにします。

CustomTableViewCell.swift
let lowerToolbar = LowerToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
lowerToolbar.initToolbarButton(item: item!)
lowerToolbar.delegate = self
textView.inputAccessoryView = lowerToolbar

上記で設定しているlowerToolbarの中身がこちらになります

LowerToolbar.swift
protocol LowerToolbarDelegete: class {
    func onTouchToolbarButton(selectedTimeInterval: Int)
}

class LowerToolbar: UIView {
    @IBOutlet weak var toolbar: UIToolbar!
    var upperToolbar: UIView?
    var isHidenUpperToolbar: Bool = true
    var upperToolbarCenterY: CGFloat = 0
    weak var delegate: LowerToolbarDelegete! = nil
    let oneHourButton = UIButton(type: .system)
    let threeHourButton = UIButton(type: .system)
    let fiveHourButton = UIButton(type: .system)
    var item: ItemModel?
    var selectedTimeInterval: Int = 0 // Last tapped button.

    override init(frame: CGRect){
        super.init(frame: frame)
        loadNib()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    func loadNib(){
        let view = Bundle.main.loadNibNamed("LowerToolbar", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
        toolbar.clipsToBounds = true

        upperToolbar = UIView(frame: CGRect(x: 0, y: -1, width: self.frame.size.width, height: 61))
        upperToolbar?.backgroundColor = UIColor.toolbar
        self.addSubview(upperToolbar!)
        self.sendSubviewToBack(upperToolbar!)
        upperToolbarCenterY = upperToolbar!.center.y
    }

    func createToolbarButton(btn: UIButton, title: String, timeInterval: Int) {
        btn.setTitle(title, for: .normal)
        btn.tag = timeInterval
        btn.layer.borderWidth = 1
        btn.layer.borderColor = UIColor.toolbarBorder.cgColor
        btn.layer.cornerRadius = 20
        btn.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
        btn.backgroundColor = UIColor.toolbarButton
        btn.addTarget(self, action: #selector(tapHoursButton), for: .touchUpInside)
        upperToolbar?.addSubview(btn)

        if self.item?.timeInterval == timeInterval {
            btn.layer.borderColor = UIColor.systemBlue.cgColor
            btn.backgroundColor = UIColor.systemBlue
            btn.tintColor = UIColor.white
        }
    }

    func initToolbarButton(item: ItemModel) {
        self.item = item

        createToolbarButton(btn: oneHourButton, title: "1hour", timeInterval: 60)
        oneHourButton.translatesAutoresizingMaskIntoConstraints = false
        oneHourButton.leadingAnchor.constraint(equalTo: upperToolbar!.leadingAnchor, constant: 20).isActive = true
        oneHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true

        createToolbarButton(btn: threeHourButton, title: "3hour", timeInterval: 180)
        threeHourButton.translatesAutoresizingMaskIntoConstraints = false
        threeHourButton.leadingAnchor.constraint(equalTo: oneHourButton.trailingAnchor, constant: 20).isActive = true
        threeHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true

        createToolbarButton(btn: fiveHourButton, title: "5hour", timeInterval: 400)
        fiveHourButton.translatesAutoresizingMaskIntoConstraints = false
        fiveHourButton.leadingAnchor.constraint(equalTo: threeHourButton.trailingAnchor, constant: 20).isActive = true
        fiveHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
    }

    func initUpperToolbar() {
        upperToolbar?.center.y = upperToolbarCenterY
        isHidenUpperToolbar = true
    }

    func decorateTappedHourButton(btn: UIButton) {
        btn.layer.borderColor = UIColor.systemBlue.cgColor
        btn.backgroundColor = UIColor.systemBlue
        btn.tintColor = UIColor.white
    }

    func decorateNormalHourButton(btn: UIButton) {
        btn.layer.borderColor = UIColor.toolbarBorder.cgColor
        btn.backgroundColor = UIColor.toolbarButton
        btn.tintColor = .systemBlue
    }

    func decorateHourButton(btn1: UIButton, btn2: UIButton, btn3: UIButton, newTimeInterval: Int) {
        if selectedTimeInterval == newTimeInterval {
            selectedTimeInterval = 0
            decorateNormalHourButton(btn: btn1)
        } else {
            selectedTimeInterval = newTimeInterval
            decorateTappedHourButton(btn: btn1)
        }
        decorateNormalHourButton(btn: btn2)
        decorateNormalHourButton(btn: btn3)
    }

    @objc func tapHoursButton(btn: UIButton) {
        switch btn.tag {
        case 60:
            decorateHourButton(btn1: oneHourButton, btn2: threeHourButton, btn3: fiveHourButton, newTimeInterval: btn.tag)
        case 180:
            decorateHourButton(btn1: threeHourButton, btn2: oneHourButton, btn3: fiveHourButton, newTimeInterval: btn.tag)
        case 400:
            decorateHourButton(btn1: fiveHourButton, btn2: oneHourButton, btn3: threeHourButton, newTimeInterval: btn.tag)
        default:
            print("no item")
        }
        delegate?.onTouchToolbarButton(selectedTimeInterval: btn.tag)
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if isHidenUpperToolbar {
            let rect = self.bounds
            return rect.contains(point)
        } else {
            var rect = self.bounds
            if rect.contains(point) {
                return rect.contains(point)
            }

            rect.origin.y -= 60
            return rect.contains(point)
        }
    }

    @IBAction func tapBellButton(_ sender: Any) {
        if isHidenUpperToolbar {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y -= 60
                self.isHidenUpperToolbar = false
                self.layoutIfNeeded()
            })
        } else {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y += 60
                self.isHidenUpperToolbar = true
                self.layoutIfNeeded()
            })
        }
    }
}

解説

LowerToolbar.swift
    func loadNib(){
        let view = Bundle.main.loadNibNamed("LowerToolbar", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
        toolbar.clipsToBounds = true

        upperToolbar = UIView(frame: CGRect(x: 0, y: -1, width: self.frame.size.width, height: 61))
        upperToolbar?.backgroundColor = UIColor.toolbar
        self.addSubview(upperToolbar!)
        self.sendSubviewToBack(upperToolbar!)
        upperToolbarCenterY = upperToolbar!.center.y
    }

LowerToolbarを初期化する際に、UpperToolbarを作成し、addSubviewしてsendSubviewToBackで背面に移動することで非表示にします。

LowerToolbar.swift
    @IBAction func tapBellButton(_ sender: Any) {
        if isHidenUpperToolbar {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y -= 60
                self.isHidenUpperToolbar = false
                self.layoutIfNeeded()
            })
        } else {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y += 60
                self.isHidenUpperToolbar = true
                self.layoutIfNeeded()
            })
        }
    }

ベルボタンをタップした際に、upperToolbarを上下に60移動しています。

LowerToolbar.swift
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if isHidenUpperToolbar {
            // upperToolbarが非表示で
            // lowerToolbarがタップされた場合
            let rect = self.bounds
            return rect.contains(point)
        } else {
            // upperToolbarが表示されているが
            // upperToolbarがタップされた場合
            var rect = self.bounds
            if rect.contains(point) {
                return rect.contains(point)
            }

            // lowerToolbarがタップされた場合
            rect.origin.y -= 60
            return rect.contains(point)
        }
    }

ポイントなのがこの部分で、upperToolbarを60上に移動した場合、親viewのlowerToolbarのframe領域からはみ出してしまうため、
upperToolbarの上に乗っているボタンのイベントを受け取れなくなってしまいます。
そこで、override func point(inside point: CGPoint, with event: UIEvent?) -> Boolをオーバーライドして
upperToolbarがタップされた場合にイベントが受け取れるようにしています。

その他気づき

今回はlowerToolbarの中でupperToolbarを生成してコードで作成しました。
これは、upperToolbarをstoryboardで個別に作成したものを使用したところ、
delegateを使用してタップされたイベントをupperToolbar->lowerToolbar->CustomTableViewCellへ移譲することができなかったためです。
(lowerToolbarでupperToolbarのdelegate設定できなかったです。)

まとめ

ツールバーの高さを動的に変更できれば一番シンプルな実装にはなるかと思いますが、
高さの変更が思ったようにできず、違う方法でトライしてみました。
アップルの純正リマインダーアプリではどうやって実装してるんですかね。気になります。
何か参考になれば嬉しいです。

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

[Swift5]年月だけのDatePicker

年月だけのDatePickerを作ろう

初記事です!
textFieldをタップして年月だけのDatePickerを使用できるようにしたいと思い作成しました。
まだまだよくわかっていないことも多く
改善すべき点などありましたら、コメントいただけますと幸いです٩( 'ω' )و

環境

・Swift 5.3
・Xcode 12.1

ソースコード

1.MonthYearPickerViewクラスを作成

import UIKit

class MonthYearPickerView: UIPickerView, UIPickerViewDelegate, UIPickerViewDataSource{

    var months: [String]!
    var years: [Int]!

    var month = Calendar.current.component(.month, from: Date())
    var year = Calendar.current.component(.year, from: Date())

    var onDateSelected: ((_ month: Int, _ year: Int) -> Void)?


    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    func commonInit() {

        var years: [Int] = []
        if years.count == 0 {
        //5年前から表示する
        let previousDate = Calendar.current.date(byAdding: .year, value: -5, to: Date())
            var year = NSCalendar(identifier: NSCalendar.Identifier.gregorian)!.component(.year, from: previousDate!)

            for _ in 1...10 {
                years.append(year)
                year += 1
            }
        }

        self.years = years

        var months: [String] = []
        var month = 0
        for _ in 1...12 {
            var calendar = Calendar.current
            calendar.locale = Locale(identifier: "ja")
            let monthSymbols = calendar.monthSymbols
            months.append(monthSymbols[month])
            month += 1
        }

        self.months = months



        self.delegate = self
        self.dataSource = self

        //現在の年月を選択状態にする
        let currentYear = Calendar(identifier: Calendar.Identifier.gregorian).component(.year, from: Date())
        let currentMonth = Calendar(identifier: Calendar.Identifier.gregorian).component(.month, from: Date())
        self.selectRow(currentMonth-1, inComponent: 1, animated: false)
        if let i = years.firstIndex(where: {$0 == currentYear}){
            self.selectRow(i, inComponent: 0, animated: false)
        }

    }

    //UIPickerDelegate&DataSource

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        switch component {
        case 0:
            return "\(years[row])年"
        case 1:
            return "\(months[row])分"
        default:
            return nil
        }
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch component {
        case 0:
            return years.count
        case 1:
            return months.count
        default:
            return 0
        }
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {

        let year = years[self.selectedRow(inComponent: 0)]
        let month = self.selectedRow(inComponent: 1)+1

        self.month = month
        self.year = year

        if let block = onDateSelected {
            block(month, year)
        }
    }
}

2.ViewControllerのViewDidLoad()でtextFieldのinputViewに設定する

 let datePickerView = MonthYearPickerView()
 datePickerView.onDateSelected = { 
   [self] (month: Int, year: Int) in
   let dateText = String(format: "%d年%02d月分",year,month)
   self.dateTextField.text = dateText
 }

 self.dateTextField.inputView = datePickerView  
 self.dateTextField.delegate = self

//textFieldタップで現在の年月をtextFieldに反映させたかったので以下を記述
extension ViewController: UITextFieldDelegate{
    func textFieldDidBeginEditing(_ textField: UITextField) {
        if textField == dateTextField {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy年MM月分"
            dateTextField.text = "\(formatter.string(from: Date()))"
        }
    }
}

できたー!! ٩( 'ω' )و

こんな感じでtextFieldをタップした時にDatePickerが表示されるようになりました。

スクショ

めちゃめちゃ参考にさせていただいたサイト

https://github.com/bendodson/MonthYearPickerView-Swift
参考というかもはやコピペですね。。。。
ありがとうございました:relaxed:

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

TextFieldと連携したDatePickerを二つ作る

はじめに

 UIDatePickerを2つ作り、TextFieldにインプットする方法をまとめてみます。

開発環境

macOS Catalina 10.15.7
Xcode 12.1
Swift 5.3

テキストフィールドの設置と変数の準備

まず、テキストフィールド2つをStoryBoard上で任意の場所に設置します。
今回は1つ目のテキストフィールドをATextField、2つ目をBTextFieldとしました。

また、以下の2つの変数を用意します。

ViewController.swift
class ViewController: UIViewController { 
    @IBOutlet weak var ATextField: UITextField!
    @IBOutlet weak var BTextFiled: UITextField!

    var APicker: UIDatePicker!
    var BPicker: UIDatePicker!

    override func viewDidLoad() {
        super.viewDidLoad()

    }
}

DatePickerを作成するメソッドの定義

次に、TextFieldに入れるDatePickerを作成するメソッドを定義します。

ViewController.swift
func makePicker(_ isA:Bool) -> UIDatePicker {
    let myPicker:UIDatePicker!
    myPicker = UIDatePicker()
    myPicker.tag = isA ? 1 : 2
    myPicker.datePickerMode = .date
    myPicker.locale = NSLocale(localeIdentifier: "ja_JP") as Locale
    myPicker.preferredDatePickerStyle = .wheels

    return myPicker
}

作成するDatePickerは2つあるので、引数にtrueかfalseをとる形にし、trueであればATextFieldに挿入するDatePickerが、falseであればBTextFiledに挿入するDatePickerが作成されます。

それぞれのDatePickerにタグを1、2とふっておきます。
DatePickerの設定についてはこのメソッド内で自由に設定します。

iOS14でのDatePickerの挙動についてはこちらの記事を参考にしました。
https://qiita.com/kj_trsm/items/a53b0b3f7e1bc7c06106

DatePickerの変更を検知するメソッドの定義

DatePickerがユーザーによって操作された際に入力された日付をテキストフィールドに表示するメソッドを定義します。

ViewController.swift
@objc internal func dateChanged(sender: UIDatePicker){
     let formatter: DateFormatter = DateFormatter()
     formatter.dateFormat = "yyyy年M月d日"

     let selectedDate = formatter.string(from: sender.date)
     if sender.tag == 1 {
         ATextField.text = selectedDate
     } else {
         BTextFiled.text = selectedDate
     }

 }

まずDate型をString型に変換するDateFormatterを用意し、変換した文字列をselectedDate変数に格納した後、senderのタグに従って入力先のテキストフィールドに文字列を代入します。

ここまでできたら、先ほど定義したmakePickerメソッド内で、myPickerにaddTargetします。

ViewController.swift
func makePicker(_ isA:Bool) -> UIDatePicker {
    let myPicker:UIDatePicker!
    myPicker = UIDatePicker()
    myPicker.tag = isA ? 1 : 2
    myPicker.datePickerMode = .date
    myPicker.locale = NSLocale(localeIdentifier: "ja_JP") as Locale
    myPicker.preferredDatePickerStyle = .wheels

    //ここ 
    myPicker.addTarget(self, action:  #selector(onDidChangeDate(sender:)), for: .valueChanged)

    return myPicker
 }

キーボードを引っ込ませる処理

このままだと、DatePickerの操作を始めるとキーボードが出っ放しになるので、
キーボード外をタップするとキーボードが引っ込む処理を行います。

ViewController.swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    ATextField.endEditing(true)
    BTextFiled.endEditing(true)
}

ViewDidLoad内でDatePickerを作成

最後に、ViewDidLoad内でDatePickerを作成するメソッドを呼び、テキストフィールドにインプットして完成です。

ViewContoller.swift
override func viewDidLoad() {
    super.viewDidLoad()

    APicker = makePicker(true)
    ATextField.inputView = APicker

    BPicker = makePicker(false)
    BTextFiled.inputView = BPicker

    }

コード全体

ViewCOntroller.swift
class ViewController: UIViewController {
    @IBOutlet weak var ATextField: UITextField!
    @IBOutlet weak var BTextFiled: UITextField!

    var APicker: UIDatePicker!
    var BPicker: UIDatePicker!

    override func viewDidLoad() {
        super.viewDidLoad()

        APicker = makePicker(true)
        ATextField.inputView = APicker

        BPicker = makePicker(false)
        BTextFiled.inputView = BPicker

    }

    func makePicker(_ isA:Bool) -> UIDatePicker {
        let myPicker:UIDatePicker!
        myPicker = UIDatePicker()
        myPicker.tag = isA ? 1 : 2
        myPicker.datePickerMode = .date
        myPicker.locale = NSLocale(localeIdentifier: "ja_JP") as Locale
        myPicker.preferredDatePickerStyle = .wheels

        myPicker.addTarget(self, action:  #selector(onDidChangeDate(sender:)), for: .valueChanged)

        return myPicker
    }

    @objc internal func onDidChangeDate(sender: UIDatePicker){
        let formatter: DateFormatter = DateFormatter()
        formatter.dateFormat = "yyyy年M月d日"

        let mySelectedDate = formatter.string(from: sender.date)
        if sender.tag == 1 {
            ATextField.text = mySelectedDate
        } else {
            BTextFiled.text = mySelectedDate
        }
    }

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


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