20201113のiOSに関する記事は11件です。

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で続きを読む

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で続きを読む

[Android][iOS]Crashlytics導入メモ

1. Androidメモ

build.gradle(app)への設定の注意

Execution failed for task
‘:app:uploadCrashlyticsMappingFileRelease’.
> Expected file collection to contain exactly one file, however, it contains no files.

とかいうエラーが出た時の原因と対処

前提
build.gradleで「minify true」を設定している場合、ビルド時に難読化が適用され、mapping.txtが作成される。
「minify false」の場合、ビルド時に難読化されず、mapping.txtは作成されない。

原因と対処
「minify false」かつ「mappingFileUploadEnabled true」を設定すると、ビルド時にmapping.txtが作成されていなくても、FirebaseSDKはmapping.txtをFirebaseにアップロードしようとして、上記のエラーが発生する。

したがって、「minify false」を設定する場合は、「mappingFileUploadEnabled false」(またはmappingFileUploadEnabled自体を書かないデフォルトfalseだから)を設定する必要があり、
「minify true」を設定する場合は、「mappingFileUploadEnabled trueまたはfalse」どちらでもよい(ただし、mappingFileUploadEnabled falseの場合、Firebaseのクラッシュログは難読化されているので、クラッシュログ見てもよくわかんなくなりそう)。

参考

Crashテスト方法

  1. 以下のコードを任意のActivityのonCreate()に仕込む
throw new RuntimeException("Test Crash"); // Force a crash
  1. ビルドしてアプリを起動して上記Activityの画面まで遷移し、クラッシュさせる。
  2. アプリをキルして再起動して、Firebaseにクラッシュレポートとしてあがってくるのをまつ(最長5分かかるらしい)

2. iOSメモ

Crashテスト方法

  1. 以下のコードをトップの画面のソースに付け加える。
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view, typically from a nib.

        let button = UIButton(type: .roundedRect)
        button.frame = CGRect(x: 20, y: 50, width: 100, height: 30)
        button.setTitle("Crash", for: [])
        button.addTarget(self, action: #selector(self.crashButtonTapped(_:)), for: .touchUpInside)
        view.addSubview(button)
    }

    @IBAction func crashButtonTapped(_ sender: AnyObject) {
        fatalError()
    }
  1. ビルドしてアプリを起動してCrashボタンをタップしてクラッシュさせる
  2. アプリをキルして再起動して、Firebaseにクラッシュレポートとしてあがってくるのをまつ(最長5分かかるらしい)

※Xcodeと端末を接続してデバッグ状態だとうまく動かないので注意。

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

【Flutter】Firebase環境構築(iOS/Android)

参考文献

iOS編

1. Firebaseプロジェクト作成

Image from Gyazo

2. iOSアプリの追加

Image from Gyazo

  • iOSバンドルID

Image from Gyazo

3. GoogleService-Info.plist

Image from Gyazo

  • Xcodeプロジェクトに追加

Image from Gyazo

4.FlutterFire プラグインを追加する

  • firebase_core導入

Image from Gyazo

  • cloud_firestore導入

Image from Gyazo

Android編

1.Androidアプリの追加

Image from Gyazo

  • Androidパッケージ名
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="sample">

2.google-services.jsonの追加

Image from Gyazo

android/app
├── build.gradle
├── google-services.json  //追加
└── src

3.Firebase SDK の追加

android/build.gradle
dependencies {
    classpath 'com.google.gms:google-services:4.3.4'  //追加
  }
android/app/build.gradle
apply plugin: 'com.google.gms.google-services'  //追加
android/app/build.gradle
android {
    defaultConfig {
        ...
        minSdkVersion 15
        targetSdkVersion 28
        multiDexEnabled true
    }
    ...
}

dependencies {
  implementation 'com.android.support:multidex:1.0.3'
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcodeでアーカイブしたファイルを再度表示する

以前にアーカイブしたiOSアプリをもう一度使いたいときってありませんか?
私は、これが分からなくてもう一度ビルドし直した経験があります...が、時間の無駄ですよね、、笑

Xcodeのアーカイブ保管庫

iOSアプリを開発して公開する際、製品版としてビルドすると、これまでにアーカイブしたファイルが
一覧になってるウィンドウが表示され、最新のものをApp Storeへアップロードする形になるかと思います。

でも、そのウィンドウの名前が分からない...
Archivesウインドウとかって名前にしておいてくれたら良いのに。。?
これを、改めて表示する方法です!

Organizerの表示方法

Xcodeでアーカイブ保管庫(Organizer)は、Windowのメニューの中にあります。

d61f3c268a8667c20fa2a89587045c1b-e1519741147630-1024x402.png

まとめ役・整理箱という意味

Organizer(オーガナイザー)とは

辞書などで調べると、組織者や主催者という訳がよく出てきますが、、以下のような意味もあるようです。

オーガナイザーはもともと「組織の勧誘活動を行う者」という意味の言葉で、社会運動の分野で使われていた言葉ですが、現在では幅広い分野で使われており、「オルグ」と略して使うことも多いです。

例えば「オーガナイザーを務める」で「まとめ役を務める」というような意味になります。

引用)https://imikaisetu.goldencelebration168.com/archives/3736

Xcodeでは、「整理箱」という意味で使われてるのだと思います。

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

SwiftUIでCoreDataを動的に検索・表示する

はじめに

SwiftUI で CoreData を利用してデータを管理するアプリを作成する中で、UIKit における UISearchBar のような機能が欲しくなりました。
ユーザーの入力に応じて動的に表示データを変えるというのは、まさに SwiftUI の得意技だと思いますが、SwiftUI には同等の機能を提供する View が用意されておらず、また、CoreData との連携方法にひと工夫必要です。

本稿ではこのような UI を作る上で必要となる、ユーザーの入力に応じて動的に CoreData のDBからデータを検索し、結果を表示する方法について説明します。

なお、本稿では、SwiftUI ならびに CoreData それぞれの基本的な説明は割愛します。

開発環境等

  • MacBook (Retina, 12inch, Early 2015)
  • macOS Catalina Version 10.15.7
  • Xcode Version 12.1 (12A7403)
  • iOS 14.1 (iPhone X 実機で動作確認済み)

課題

入門記事などでは、SwiftUIでCoreDataを利用するための基本的な実装として以下のようなコードがよく紹介されています。

ItemListView1.swift
struct ItemListView1: View {
    @Environment(\.managedObjectContext) var context
    @FetchRequest(entity: Item.entity(),
                  sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)],
                  predicate: NSPredicate(format: "title CONTAINS[C] \'hoge\'"),
                  animation: .default) var items: FetchedResults<Item>

    var body: some View {
        List {
            ForEach(items) { item in
                HStack {
                    Text(item.title!)
                }
            }
        }
    }
}

注) CoreDataを利用するために、AppDelegate.swift などで、NSPersistentCloudKitContainer オブジェクトを生成するなどの準備も必要ですが、本稿では CoreData 利用の基本説明は割愛しています。

これは、文字列型の title プロパティに文字列"hoge"を含む item オブジェクトをDBから取得しリスト表示する例ですが、検索する文字列は固定となっています。

やりたいのは、TextField でユーザーが入力した文字列をもとに、動的にデータを検索・取得することです。どうしたら良いでしょうか?

試行錯誤

まず考えられるのは以下のようなコードです。
TextField でユーザーが入力したデータを serchWord に代入し、それを@FetchRequestpredicate に設定するという考え方です。

※このコードではエラーとなります。

ItemListView2.swift
struct ItemListView2: View {
    @Environment(\.managedObjectContext) var context
    @FetchRequest(entity: Item.entity(),
                  sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)],
                  predicate: NSPredicate(format: "title CONTAINS[C] %@", searchWord),
                  animation: .default) var items: FetchedResults<Item>

    @State private var searchWord: String

    var body: some View {
        VStack {
            TextField("search word", text: $searchWord)
            List {
                ForEach(items) { item in
                    HStack {
                        Text(item.title!)
                    }
                }
            }
        }
    }
}

実際にコードを書いてみると、@FetchRequest()predicate を設定する部分でエラーとなります。
プロパティの初期化に、初期化されていない他のプロパティを使うことができないのです。

では、どうすれば良いのでしょうか?

解決策

パターンA

まず、検索文字列を入力する View と、それをもとに List 表示する View を sub view として分けます。
後者の sub view の init() で、検索文字列を受け取り、FetchRequest を設定します。

init() の中で、FetchRequest を設定するのは、 self.items ではなく、self._items であるところがミソです。
sel.items は get only なので、何かを set するにはその本体である self._items に対して行う必要があるのです。

ItemListView3.swift
struct ItemListView3: View {
    @Environment(\.managedObjectContext) var context
    @State private var searchWord: String = ""

    var body: some View {
        VStack {
            TextField("search word", text: $searchWord)

            ItemListView3sub(searchWord: searchWord)
        }
    }
}

struct ItemListView3sub: View {
    @Environment(\.managedObjectContext) var context
    @FetchRequest var items: FetchedResults<Item>

    var searchWord: String

    init(searchWord: String) {
        self.searchWord = searchWord

        self._items = FetchRequest(entity: Item.entity(),
                                   sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)],
                                   predicate: NSPredicate(format: "title CONTAINS[C] %@", searchWord),
                                   animation: .default)
    }

    var body: some View {
        List {
            ForEach(items) { item in
                HStack {
                    Text(item.title!)
                }
            }
        }
    }
}

パターンB

まず、検索文字列を入力する View と、それをもとに List 表示する View を分ける所は一緒ですが、子View に対して、FetchRequest を丸ごと渡します。

ItemListView4.swift
struct ItemListView4: View {
    @Environment(\.managedObjectContext) var context
    @State private var searchWord: String = ""

    var body: some View {
        VStack {
            TextField("search word", text: $searchWord)

            ItemListView4sub(items: FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)], predicate: NSPredicate(format: "title CONTAINS[C] %@", searchWord)))
        }
    }
}

struct ItemListView4sub: View {
    @Environment(\.managedObjectContext) var context
    @FetchRequest var items: FetchedResults<Item>

    var body: some View {
        List {
            ForEach(items) { item in
                HStack {
                    Text(item.title!)
                }
            }
        }
    }
}

パターンAに比べパターンBの方がシンプルに思えます。
ただし、パターンAの方は、例えば、受け取った文字列が空文字列だった場合は検索条件を変える、などといったキメの細かい処理を行うことが可能になります。

最後に

SwiftUI や CoreData はまだまだ情報が少なく、ちょっと突っ込んだ使い方をしたい場合、意外と情報が見つからないことが多いように思います。

この記事が少しでもお役に立てれば幸いです。

参考

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

キーチェーンとそのコマンドたち

きっかけ

最近キーチェーンを調べなければならないシーンがあったのですが、circleCI上での話だったのでCUIしか使えず。。。
GUIでしかキーチェーンの操作したことがなかった僕はとても困ったので記事にしました。

キーチェーン概要

赤い枠の部分がキーチェーンたちで、「ログイン」が1個のキーチェーンになってます。
そして青い枠で、選択されているキーチェーン内にある項目の何を表示するかの絞り込みを行ってます。

システムが認識できるキーチェインは2つで、
システムのキーチェインが1つとユーザーのキーチェインが1つのみになります。たぶん。。

キーチェイン作成

security create-keychain {{キーチェーンパス}}

キーチェイン一覧

security list-keychains

システム側とユーザー側とで設定されているキーチェイン2つを表示します。
-d u オプションでユーザーのキーチェーンが確認できます。

読み込むキーチェインを追加します。

security list-keychains -s {{キーチェーンパス}}

キーチェインに証明書を追加します。

security import {{証明書パス}} -k {{キーチェーンパス}} -P {{password}} -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

-Tオプションで、パスワードなしで許可するアプリを設定します。複数指定可能です。

キーチェインの設定を行います。

security set-keychain-settings -t 3600 -l {{キーチェーンパス}}

-lオプションは、スリープした時にキーチェーンをロックします。
-t 3600オプションは、3600秒後にキーチェーンをロックします。

実際にやってみよう!

キーチェーン一覧

まずはキーチェーン一覧を見てみます。
/Users/maruiyugo/Library/Keychains/login.keychain-dbが画像の「ログイン」というキーチェーンです。
/Library/Keychains/System.keychainが「システム」キーチェーンですね。

$ security list-keychains
    "/Users/maruiyugo/Library/Keychains/login.keychain-db"
    "/Library/Keychains/System.keychain"

キーチェーン作成と設定

marui.keychain-dbキーチェーンを作成し、キーチェーンの読み込みを行いました。
最後にキーチェーンのリストを表示させています。
するとlogin.keychain-dbmarui.keychain-dbに入れ替わったではありませんか!
キーチェーンアクセスをみても「ログイン」が「marui」になっています。

$ security create-keychain ~/Library/Keychains/marui.keychain-db
    password for new keychain:
    retype password for new keychain:
$ security list-keychains -s ~/Library/Keychains/marui.keychain-db
$ security list-keychains
    "/Users/maruiyugo/Library/Keychains/marui.keychain-db"
    "/Library/Keychains/System.keychain"

パスワードの保存などはまだしていないため中身は空っぽです。

キーチェインに証明書を追加

適当なp12ファイルを追加してみました。

$ security import {{p12}} -k ~/Library/Keychains/marui.keychain-db -P {{password}} -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

一覧にp12ファイルが表示されました。

キーの中身を見てみると、オプションで指定した「codesign」「security」「productbuild」が許可されています。

まとめ

キーチェーンは今まで流されるままに使っていたので、今回調べて詳しくなれてよかったです。
記事がよかった!とかためになった!とか思ったら、励みになるのでLGTMおしてください!ないてよろこびます(๑>◡<๑)

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

子供用の学習アプリケーションを作る(1)

はじめに

昨日、筋萎縮性側索硬化症(ALS)と呼ばれる病気についての記事を見かけ、以前より知っている病気ではあったもののなぜ、この病気になってしまうのかについては、知らなかった。
ネットで調べてみたところ、様々なサイトで解説されているものの、言葉が難しかったり、文字数が多く読むのが大変であった。

そこで、そのような知識を分かりやすく、まとめ尚且つ大人から子供まで使えるような学習アプリケーションを自作してみました。

実装

まず、環境構築については、下記の記事を見ていただければと思います。
参考: React Nativeをはじめる(環境構築)

ディレクトリ構成

使用したUI component, ライブラリ, フリーイラスト

UI component
1.React-native-elements
React-Native-Elementsは、フリーのクロスプラットフォームのUIcomponentです。
色々な開発者が作成したcomponentが集められ、クオリティもかなり高いです。

2.React-native-paper
React-native-paperは、使いやすく設計されているUIcomponentです。
componentは、Googleのマテリアル基準に準拠しているのでデザイン性がかなり高いという感想です。

ライブラリ
1.React Navigation
React Navigationは、画面遷移のライブラリです。
なぜ、このライブラリが必要かというと、Reactでは画面遷移のための機能が用意されていないからです。

React Navigationでは3つの方法で画面遷移ができます。

1.stack:スライドしながら画面遷移する方法
2.tabs:タブを選択することで画面遷移する方法
3.drawer:画面端から現れるメニューで画面遷移する方法

今回は、1の方法で実装を行いました。

フリーイラスト
1.manypixels
manypixelsは、フラットなイラストをフリーでdlできます。

App.js

このファイルではメインとなるページの処理を記載しています。

App.js
import 'react-native-gesture-handler';
import * as React from 'react';
import {Image, SafeAreaView, ScrollView, StyleSheet} from 'react-native';
import {Text, Card, Button} from 'react-native-elements';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import AppHeader from './components/header';
import contentsSelect from './components/contentsSelect';

const HomeScreen = ({navigation}) => {
  return (
    <SafeAreaView style={styles.container}>
      <ScrollView>
        <AppHeader />
        <Text h2 style={styles.hearderMessage}>
          For explorer who want to know the unknown
        </Text>
        <Text style={styles.hearderMessageBody}>
          ilearnはさまざまなトピックスをまとめ{'\n'}発信する場所だよ
        </Text>

        <Card>
          <Card.Title>探検をはじめよう</Card.Title>
          <Card.Divider />
          <Image
            source={require('../public/img/teach.png')}
            style={styles.cardImage}
          />
          <Text style={styles.cardText}>
            医学,科学,数学,コンピュータサイエンス,宇宙,{'\n'}歴史をまなぼう
          </Text>

          <Button
            style={styles.cardButtonText}
            title="出発する?"
            onPress={() => navigation.navigate('Contents')}
          />
        </Card>
      </ScrollView>
    </SafeAreaView>
  );
};

const Stack = createStackNavigator();

const App = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Contents" component={contentsSelect} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  headerImg: {
    width: 90,
    height: 30,
    top: 60,
  },
  hearderMessage: {
    marginTop: 100,
    textAlign: 'center',
  },
  hearderMessageBody: {
    flex: 1,
    top: 20,
    marginBottom: 60,
    textAlign: 'center',
    fontWeight: 'bold',
    color: '#93a5b1',
  },
  cardImage: {
    width: 350,
    height: 350,
  },
  cardText: {
    marginBottom: 10,
  },
  cardButtonText: {
    marginTop: 20,
    marginBottom: 20,
  },
});

export default App;

header component

名前から察するようにヘッダーについてまとめたファイルになります。

header.js
import React from 'react';
import {View, StyleSheet, Image} from 'react-native';

const AppHeader = () => {
  return (
    <View>
      <Image
        source={require('../../public/img/Header.png')}
        style={styles.headerImg}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  headerImg: {
    width: 90,
    height: 30,
    marginLeft: 20,
    top: 30,
  },
});

export default AppHeader;

contents component

こちらのcontents componentは、ユーザが実際に何のコンテンツについて学びたいかを選ばせるページの処理をまとめています。

contentsSelect.js
import React from 'react';
import {SafeAreaView, ScrollView, StyleSheet} from 'react-native';
import {Card, Title} from 'react-native-paper';
import AppHeader from './header';

const contentsSelect = () => {
  return (
    <SafeAreaView style={styles.container}>
      <ScrollView>
        <AppHeader />
        <Card style={styles.cardPadding}>
          <Card.Content>
            <Title style={styles.cardTitle}>宇宙って</Title>
            <Card.Cover
              source={require('../../public/img/alien.png')}
              style={styles.cardImg}
            />
          </Card.Content>
        </Card>
        <Card style={styles.cardPadding}>
          <Card.Content>
            <Title style={styles.cardTitle}>ALSって知ってる</Title>
            <Card.Cover
              source={require('../../public/img/health.png')}
              style={styles.cardImg}
            />
          </Card.Content>
        </Card>
        <Card style={styles.cardPadding}>
          <Card.Content>
            <Title style={styles.cardTitle}>Falcon 9がすごい</Title>
            <Card.Cover
              source={require('../../public/img/startup_isometric.png')}
              style={styles.cardImg}
            />
          </Card.Content>
        </Card>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  cardImg: {
    height: 300,
  },
  cardPadding: {
    marginBottom: 20,
  },
  cardTitle: {
    fontWeight: 'bold',
  },
});

export default contentsSelect;

動作

ezgif.com-gif-maker.gif

次回

次回は、コンテンツ選択した後の詳細画面を作り込んでいこうと思います。

おわり

かなり気合を入れて作成を始めたので、随時記事も更新していこうと思います。
また、React-vativeをさわってみたい方に対し、開発コードがお役に立てば幸いです。

参考

React-native
React-native-elements
React Navigation
React-native-paper
manypixels
難病情報センター: ALS

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