20200115のSwiftに関する記事は13件です。

【swift5】自作電卓アプリを作ってみた

まずは全体像から

GIFがこちら

uMMu8OnW.gif

全体のデザインがこちら

スクリーンショット 2020-01-15 22.03.16.png

全体のコード

githubはこちらから
https://github.com/sventouz/calculator

import UIKit

class ViewController: UIViewController {

    var numberOnScreen:Int = 0
    var previousNumber:Int = 0
    var performingMath = false
    var operation = 0

    @IBOutlet weak var label: UILabel!

    @IBAction func numbers(_ sender: UIButton) {

        if performingMath == true {
            label.text = String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
            performingMath = false
        }
        else {
            label.text = label.text! + String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
        }

    }

    @IBAction func buttons(_ sender: UIButton) {

        if label.text != "" && sender.tag != 11 && sender.tag != 16{
            previousNumber = Int(label.text!)!
            if sender.tag == 12{ // ÷
                label.text = "÷";
            }
            else if sender.tag == 13{  // ×
                label.text = "×";
            }
            else if sender.tag == 14{  // -
                label.text = "-";
            }
            else if sender.tag == 15{  // +
                label.text = "+";
            }
            operation = sender.tag
            performingMath = true;
        }
        else if sender.tag == 16 // = が押された時の処理
        {
            if operation == 12{
                label.text = String(previousNumber / numberOnScreen)
            }
            else if operation == 13{
                label.text = String(previousNumber * numberOnScreen)
            }
            else if operation == 14{
                label.text = String(previousNumber - numberOnScreen)
            }
            else if operation == 15{
                label.text = String(previousNumber + numberOnScreen)
            }
        }
        else if sender.tag == 11{ // C が押された時の処理
            label.text = ""
            previousNumber = 0;
            numberOnScreen = 0;
            operation = 0;
        }

    }

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

}

コードを説明していきます

コードの接続

まずコードでは表示されていませんが、0〜9の数字を

@IBAction func numbers(_ sender: UIButton) {
}

のなかにドラッグアンドドロップします。

こちらも同様に+, -, ÷, × をドラッグアンドドロップ。

@IBAction func buttons(_ sender: UIButton) {
}

IBアクションではありません。connect actionです。

tag追加

tagを追加していきます。

0には1を

1には2をつけていき9に10がつけばOK

次は

Cに11

÷に12

と続き

=が16になれば完璧です。

続き

@IBAction func numbers(_ sender: UIButton) {
  // この中が数字をクリックしたときに動く場所
}
@IBAction func buttons(_ sender: UIButton) {
  // この中が数字以外の四則関数をクリックしたときに動く場所
}

あとは全体のコードを見ながら各自コードを解読していってください。

感想

電卓なんか簡単でしょ?って思っていたのですが難しかったです。笑

もっとシンプルなのを作っていき最終的には複雑なものを作っていければいいと思う。

その過程ではアウトプットを忘れずに!

参考

https://blog.codecamp.jp/iphone-app-develope-original-calculator

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

【swift5】自作電卓アプリを作ってみよう(完全版)

まずは全体像から

GIFがこちら

uMMu8OnW.gif

全体のデザインがこちら

スクリーンショット 2020-01-15 22.03.16.png

全体のコード

githubはこちらから
https://github.com/sventouz/calculator

import UIKit

class ViewController: UIViewController {

    var numberOnScreen:Int = 0
    var previousNumber:Int = 0
    var performingMath = false
    var operation = 0

    @IBOutlet weak var label: UILabel!

    @IBAction func numbers(_ sender: UIButton) {

        if performingMath == true {
            label.text = String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
            performingMath = false
        }
        else {
            label.text = label.text! + String(sender.tag-1)
            numberOnScreen = Int(label.text!)!
        }

    }

    @IBAction func buttons(_ sender: UIButton) {

        if label.text != "" && sender.tag != 11 && sender.tag != 16{
            previousNumber = Int(label.text!)!
            if sender.tag == 12{ // ÷
                label.text = "÷";
            }
            else if sender.tag == 13{  // ×
                label.text = "×";
            }
            else if sender.tag == 14{  // -
                label.text = "-";
            }
            else if sender.tag == 15{  // +
                label.text = "+";
            }
            operation = sender.tag
            performingMath = true;
        }
        else if sender.tag == 16 // = が押された時の処理
        {
            if operation == 12{
                label.text = String(previousNumber / numberOnScreen)
            }
            else if operation == 13{
                label.text = String(previousNumber * numberOnScreen)
            }
            else if operation == 14{
                label.text = String(previousNumber - numberOnScreen)
            }
            else if operation == 15{
                label.text = String(previousNumber + numberOnScreen)
            }
        }
        else if sender.tag == 11{ // C が押された時の処理
            label.text = ""
            previousNumber = 0;
            numberOnScreen = 0;
            operation = 0;
        }

    }

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

}

コードを説明していきます

コードの接続

まずコードでは表示されていませんが、0〜9の数字を

@IBAction func numbers(_ sender: UIButton) {
}

のなかにドラッグアンドドロップします。

こちらも同様に+, -, ÷, × をドラッグアンドドロップ。

@IBAction func buttons(_ sender: UIButton) {
}

IBアクションではありません。connect actionです。

tag追加

tagを追加していきます。

0には1を

1には2をつけていき9に10がつけばOK

次は

Cに11

÷に12

と続き

=が16になれば完璧です。

続き

@IBAction func numbers(_ sender: UIButton) {
  // この中が数字をクリックしたときに動く場所
}
@IBAction func buttons(_ sender: UIButton) {
  // この中が数字以外の四則関数をクリックしたときに動く場所
}

あとは全体のコードを見ながら各自コードを解読していってください。

感想

電卓なんか簡単でしょ?って思っていたのですが難しかったです。笑

もっとシンプルなのを作っていき最終的には複雑なものを作っていければいいと思う。

その過程ではアウトプットを忘れずに!

参考

https://blog.codecamp.jp/iphone-app-develope-original-calculator

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

Swift: ひらがな・カタカナ文字列の五十音順ソートで躓いた

はじめに

APIレスポンスから受け取ったデータを、五十音順にソートしてリスト表示するというものを行いました。
実際には不要だったのでプロダクトコードには残していませんが、ちょっとややこしくて時間食った&やった過程を無駄にしたくないので備忘録。

やること

  • [{"name": "お店の名前", "kana": "フリガナ"}]というリストを五十音順ソート
  • リスト表示するのはnameのお店の名前のみ

内容

五十音順と言えども、以下のような順序の指定がありました。

アイウエオ順→長音→清音→濁音→半濁音→小書きの仮名→ナミ字

  • 長音:  ー (直前の母音)
  • 静音:  ア とか ハ とか
  • 濁音:  ガ とか ダ とか
  • 半濁音: パ とか ペ とか
  • 小文字: ョ とか ッ とか
  • ナミ字: 〜 (直前の母音)

Swiftには.sorted()メソッドが用意されているので、配列や辞書などはこれを用いることができます。

// array
let array: [String] = ["パン", "バ", "ハ", "ハ〜シ", "〜バ", "ハーシ", "ハバ", "ハハ", "ハパーシ", "ハョーニ", "ーバ"]
let sortedArray = array.sorted()
print("sortedArray \(sortedArray)")
// sortedArray ["〜バ", "ハ", "ハ〜シ", "ハハ", "ハバ", "ハパーシ", "ハョーニ", "ハーシ", "バ", "パン", "ーバ"]

// dictionary
let dict = ["1": "パン", "2": "バ", "3": "ハ", "4": "ハ〜シ", "5": "〜バ", "6": "ハーシ", "7": "ハバ", "8": "ハハ", "9": "ハパーシ", "10": "ハョーニ", "11": "ーバ"]
let sortedDict = dict.sorted { $0.value < $1.value }
print("sortedDict \(sortedDict)")
// sortedDict [(key: "5", value: "〜バ"), (key: "3", value: "ハ"), (key: "4", value: "ハ〜シ"), (key: "8", value: "ハハ"), (key: "7", value: "ハバ"), (key: "9", value: "ハパーシ"), (key: "10", value: "ハョーニ"), (key: "6", value: "ハーシ"), (key: "2", value: "バ"), (key: "1", value: "パン"), (key: "11", value: "ーバ")]

辞書型の戻り値は純粋な辞書型ではなく、
[(key: Data, value: Data)]
という、keyとvalueを持ったタプルの配列で返ってきます。

ちなみに、ひら・カナ混合だと、降順だとひら→カナの順です。


しかし、既存のメソッドでソートされる順序は

アイウエオ順→ナミ字→清音→濁音→半濁音→小書きの仮名→長音

のようにナミ字の判定が最前列、長音が最後列といった風に上記の期待とは一部逆になります。
(厳密にいうとナミ字がよりも前に来ます)

今回必要とされる条件は

  1. アイウエオ順→長音→清音→濁音→半濁音→小書きの仮名→ナミ字の順
  2. 規定に沿ってソート、お店の名前だけディスプレイ

なので

1. 静音 <-> ナミ字 をスワップしてソートする
2. kanaだけソートするわけにいかないので、辞書型リストにしてセットでソートする

で対応します。

コード

struct Shop {
    let name: String
    let kana: String
}

let shopList: [Shop] = [Shop(name: "1", kana: "パン"),
                        Shop(name: "2", kana: "バ"),
                        Shop(name: "3", kana: "ハ"),
                        ...]
// 全部書くの大変なので、以下のデータを元にしてAPIレスポンス時にshopListを作成していると思ってください
// ["1": "パン", "2": "バ", "3": "ハ", "4": "ハ〜シ", "5": "〜バ", "6": "ハーシ", "7": "ハバ", "8": "ハハ", "9": "ハパーシ", "10": "ハョーニ", "11": "ーバ"]

private var shopNames: [String] {
    // name と kana をセットにするため辞書型にする
    let baseDict = shopList.flatMap { [$0.name: $0.kana] }
    // ”〜” と ”ー” を入れ替えてソートする
    let replaced = replaceWaveAndLong(dict: baseDict).sorted { $0.value < $1.value }
    // ソートしたら元のデータに戻すために、再度”〜” と ”ー” を入れ替える
    let dict = replaceWaveAndLong(dict: replaced)

    print("dict \(dict)")
    // dict [(key: "11", value: "ーバ"), (key: "3", value: "ハ"), (key: "6", value: "ハーシ"), (key: "8", value: "ハハ"), (key: "7", value: "ハバ"), (key: "9", value: "ハパーシ"), (key: "10", value: "ハョーニ"), (key: "4", value: "ハ〜シ"), (key: "2", value: "バ"), (key: "1", value: "パン"), (key: "5", value: "〜バ")]

    // name の部分だけ取り出す
    return dict.map { $0.key }
}

// 指定のソート順に合わせるための ”〜” と ”ー” を入れ替えるメソッド
func replaceWaveAndLong(dict: [(key: String, value: String)]) -> [(key: String, value: String)] {
    let replaced = dict.map { elem -> (key: String, value: String) in
        let wave = "〜", long = "ー"

        if elem.value.contains(wave) {
            return (key: elem.key,
                    value: elem.value.replacingOccurrences(of: wave, with: long))
        }
        if elem.value.contains(long) {
            return (key: elem.key,
                    value: elem.value.replacingOccurrences(of: long, with: wave))
        }
        return elem
    }
    return replaced
}

といった感じです。

まとめ

長音は大体の場合、前の文字の母音になるので、大体順序は先頭に来るように指定されることが多そう。でも既存だと後ろに行ってしまうので、このように手を加える必要がありそう。

ちなみにここに辿り着くまで、長音が来たら前の文字をローマ字化して、その母音と同じ音に変換してとか
unicodeで文字をスイッチしたりとか、enumで全部作ってやろうかなど思いましたが、上記の条件ならこれで大丈夫そうでした。

実装することはなかったけど少し勉強になりました、ちゃんちゃん。

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

【swift 5】アプリのバックグラウンドにユーザーの選んだ画像を設定する

環境

Xcode 11.3
swift 5.1.3
CocoaPods 1.8.4

要件

機能要件

アプリのバックグラウンドに、ユーザーの選んだ画像を設定する機能

仕様

  • 「背景に画像を設定するボタン」をタップ
  • 背景画像設定画面の表示
  • カメラロール内の画像一覧を表示
  • 画像をタップすることで、拡大表示
  • オッケーボタンを押すことで、画像がアプリ内部に保存される
  • その段階で、アプリの背景に選択した画像が表示される
  • 設定完了後、「背景に画像を設定するボタン」が表示されているもとのページに戻る

設計

写真選択

背景に画像を設定するボタンタップ後、背景画像設定用の画面を表示。
カメラロール内の画像一覧を表示し、画像を選択させる。
選択後、選択した画像が背景に拡大表示されるようにする。

storyboard上の設定

TopViewControllerに対して

  • UIButtonを追加
  • storyboardIDを、topViewに設定

スクリーンショット 2020-01-13 19.02.29.png

SetBackgroundImageViewControllerに対して

  • 背景画像設定用の新規コントローラーの作成(SetBackgroundImageViewController)
  • UIButton,imageViewを設置
  • storyboardIDをbackgroundImageViewに設定

スクリーンショット 2020-01-15 21.11.43.png

コードの記述

TopViewController.swift
import UIKit

class TopViewController: UIViewController {

    // 背景image設定ページへ
    // Btnが押されたときのaction
    @IBAction func showImageSelection(_ sender: Any) {
        // コントローラーの指定
        let backgroundImageController = self.storyboard?.instantiateViewController(withIdentifier: "backgroundImageView") as! SetBackgroundImageViewController

        // 全画面表示で画面遷移
        backgroundImageController.modalPresentationStyle = UIModalPresentationStyle(rawValue: 0)!
        self.present(backgroundImageController, animated: false, completion: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("user view did load")
    }
}

SetBackgroundImageViewController
import UIKit

class SetBackgroundImageViewController: UIViewController {

    // 背景設定
    @IBOutlet weak var backgroundImageView: UIImageView!
    var backgroundImage:UIImage!
    var themaColor:UIColor!

    // ボタンのoutlet接続およびaction接続
    // 戻るボタン
    @IBOutlet weak var rerturnBtn: boundButton!
    @IBAction func returnBtnPushed(_ sender: Any) {
    }
    // 画像選択ボタン
    @IBOutlet weak var selectBtn: UIButton!
    @IBAction func selectBtnPushed(_ sender: Any) {
    }
    // 決定ボタン
    @IBOutlet weak var setBtn: UIButton!
    @IBAction func setBtnPushed(_ sender: Any) {
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // imagePickerを表示
        addImagePickerView()  
    }
}

// imagePickerViewの設定用
extension SetBackgroundImageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
    func addImagePickerView() {
        print("pushed image!")
        //imagePickerViewを表示する
        let pickerController = UIImagePickerController()
        pickerController.sourceType = .photoLibrary
        pickerController.delegate = self
        self.present(pickerController, animated: true, completion: nil)
    }

    // pickerの選択がキャンセルされた時の処理
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
         dismiss(animated: true, completion: nil)
    }
}


選択画像表示

画像が選択されたら、コントローラーのimageViewに表示されるようにする。

SetBackgroundImageViewController.swift
// imagePickerViewの設定用
extension SetBackgroundImageViewController {
    // 以下の関数を追加
    // 写真が選択された時の処理
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

        // 選択されたimageを取得
        guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return}

        // imageをimageViewに設定
        backgroundImage = selectedImage
        backgroundImageView.image = backgroundImage

        // imagePickerの削除
        self.dismiss(animated: true, completion: nil)
    }
}

選択画像保存

決定ボタン押下時、選択されている画像をアプリ内に保存。
処理終了後に、topページに戻る。

SetBackgroundViewController.swift
import UIKit

class SetBackgroundImageViewController: UIViewController {

    // 背景設定
    @IBOutlet weak var backgroundImageView: UIImageView!
    var backgroundImage:UIImage!
    var themaColor:UIColor!

    // ボタン接続
    // 戻るボタン
    @IBOutlet weak var rerturnBtn: boundButton!
    @IBAction func returnBtnPushed(_ sender: Any) {
    }
    // もう一度選ぶボタン
    @IBOutlet weak var selectBtn: UIButton!
    @IBAction func selectBtnPushed(_ sender: Any) {
    }
    // 確定ボタン
    @IBOutlet weak var setBtn: UIButton!
    @IBAction func setBtnPushed(_ sender: Any) {
        // ここから追記
        // backgroundImage.pngという名前で保存
        let imagePath = self.fileInDocumentsDirectory(filename: "backgroundImage.png")
        if saveImage(image: backgroundImage, path: imagePath) {
            // 画像を設定
            let topVC = self.presentingViewController as! TopViewController
            topVC.backgroundImage.image = backgroundImage

            // もとの画面(TopViewController)に戻る
            self.dismiss(animated: false, completion: nil)

        } else {
            backgroundImage = nil
        }
        // ここまで追記
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // imagePickerを表示
        addImagePickerView()  
    }
}

// ここから追記
// imageの保存関数
extension BackgroundImageViewController {
    // DocumentディレクトリのfileURLを取得
    func getDocumentsURL() -> NSURL {
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL
        return documentsURL
    }

    // ディレクトリのパスにファイル名をつなげてファイルのフルパスを作る
    func fileInDocumentsDirectory(filename: String) -> String {
        let fileURL = getDocumentsURL().appendingPathComponent(filename)
        return fileURL!.path
    }

    // ファイルに書き込み
    func saveImage (image: UIImage, path: String ) -> Bool {
        let pngImageData = image.pngData()
        do {
            try pngImageData!.write(to: URL(fileURLWithPath: path), options: .atomic)
        } catch {
            print(error)
            return false
        }
        return true
    }
}
// ここまで追記

// imagePickerViewの設定用
extension SetBackgroundImageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
    func addImagePickerView() {
        print("pushed image!")
        //imagePickerViewを表示する
        let pickerController = UIImagePickerController()
        pickerController.sourceType = .photoLibrary
        pickerController.delegate = self
        self.present(pickerController, animated: true, completion: nil)
    }

    // pickerの選択がキャンセルされた時の処理
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
         dismiss(animated: true, completion: nil)
    }

    // 写真が選択された時の処理
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

        // 選択されたimageを取得
        guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return}

        // imageをimageViewに設定
        backgroundImage = selectedImage
        backgroundImageView.image = backgroundImage

        // imagePickerの削除
        self.dismiss(animated: true, completion: nil)
    }
}


保存画像のロード・表示

以下の関数を用意しておき、背景画像を表示させたいところで呼び出す。

// imageをload
func loadBackgroundImage() -> UIImage? {
    let image = UIImage(contentsOfFile: fileInDocumentsDirectory(filename: "backgroundImage.png"))
    if image == nil {
        print("backgroundImage is missing")
    }
    return image
}

参考ページ

teratail : エラー:which is already presenting (null)の対処法
qiita : 【Swift4】URL先の画像をアプリ内に保存&ロードする
HatenaBlog : iOSでアプリ内部(Document)に画像(UIImage)を保存する方法【iOSアプリ開発】

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

iPadのplaygroundsはプログラミングの学習ツールではなくガチの開発環境だった

パッと見で、子供でもプログラミングを学ぶことができる学習ツールに見える iPad の Playgrounds ですが、各種フレームワークも呼び出せるガチの開発環境だったので、SceneKitで3Dオブジェクトを表示するまでを紹介します。

新規プロジェクトの作成

E7B50CD2-B173-4752-9B36-BB7088199426.jpeg

Playgrounds を起動したら、左上のアイコンをタップして新しい空のプロジェクトを作成します。

コードの入力

8314A241-833A-4898-9706-C15D6F09DF18.png

ダブルタップでソフトウェアキーボードが出てくるので下記のコードを入力します。

内容は SceneKit を使って、ライトとカメラと立方体を作成して配置しているだけのシンプルなものです。

scenekit01.swift
import PlaygroundSupport
import UIKit
import SceneKit

var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 1000, height: 200))
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = .black
sceneView.allowsCameraControl = true
PlaygroundPage.current.liveView = sceneView

var lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.light?.intensity = 1000
lightNode.light?.shadowMode = .deferred
lightNode.position = SCNVector3(x: 2, y: 2, z: 2)
scene.rootNode.addChildNode(lightNode)

var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
scene.rootNode.addChildNode(cameraNode)

var material = SCNMaterial()
material.diffuse.contents = UIColor.red

var box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.2)
var boxNode = SCNNode(geometry: box)
scene.rootNode.addChildNode(boxNode)

box.materials = [material]
boxNode.rotation = SCNVector4(x: 1.0, y: 1.0, z: 0.0, w: 0.0)
boxNode.scale = SCNVector3(x: 1.0, y: 1.0, z: 1.0)

実行してみる

AD7687D8-A20A-44FA-8982-2CE63B835561.png

allowsCameraControll を True にしているので、ドラッグで回転とかをする事ができます。

最後に

エディタの反応が少し悪くイラつく事もあるのですが、 iPad だけで、ガチの swift コードが書けることに感動しました。

これで自宅外でいつでもプログラミングができます!

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

Swift:OAuthSwiftを使ってQiita API v2をいじる

macOSでOAuthSwiftを使ってQiita APIにOAuth 2.0で認証する方法

1. Qiita側の準備

Qiitaの設定ページへ行き,新規にアプリケーションを登録して,Client IDとClient Secretを入手します.
リダイレクト先のURLは[任意の文字列] + ://oauth-callbackにして,メモしておきます.
qiita.png

2. プロジェクトの下準備

URL Typeを追加します.URL Schemesに先ほどのリダイレクト先の任意の文字列を入力します.
スクリーンショット 2020-01-15 17.02.10.png

3. コーディング

OAuth認証をするためのWebViewController

OAuthWebVC.swift
import Foundation
import WebKit
import OAuthSwift

class OAuthWebVC: OAuthWebViewController, WKNavigationDelegate {

    var targetURL: URL?
    let webView = WKWebView()
    var cancelBtn: NSButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        webView.navigationDelegate = self
        webView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(webView)
        cancelBtn = NSButton(title: "Cancel", target: self, action: #selector(cancel))
        cancelBtn!.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(cancelBtn)

        webView.leadingAnchor.constraint(equalTo:  self.view.leadingAnchor).isActive = true
        webView.trailingAnchor.constraint(equalTo:  self.view.trailingAnchor).isActive = true
        webView.topAnchor.constraint(equalTo:  self.view.topAnchor).isActive = true
        webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -40.0).isActive = true
        cancelBtn!.widthAnchor.constraint(equalToConstant: 82.0).isActive = true
        cancelBtn!.heightAnchor.constraint(equalToConstant: 32.0).isActive = true
        cancelBtn!.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20.0).isActive = true
        cancelBtn!.centerYAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20.0).isActive = true
    }

    override func handle(_ url: URL) {
        targetURL = url
        super.handle(url)
        self.loadAddressURL()
    }

    func loadAddressURL() {
        guard let url = targetURL else {
            return
        }
        let req = URLRequest(url: url)
        webView.load(req)
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = navigationAction.request.url {
            if url.host == "oauth-callback" {
                OAuthSwift.handle(url: url)
                decisionHandler(WKNavigationActionPolicy.cancel)
                self.dismissWebViewController()
                return
            }
        }
        decisionHandler(WKNavigationActionPolicy.allow)
    }

    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        Swift.print(error.localizedDescription)
        self.dismissWebViewController()
    }

    @objc func cancel() {
        self.dismissWebViewController()
    }

}

認証とユーザ情報の取得を促すベースのViewController

ViewController.swift
import Cocoa
import OAuthSwift

class ViewController: NSViewController, OAuthWebViewControllerDelegate {

    var oauthswift: OAuth2Swift?
    var client: OAuthSwiftClient?
    lazy var webVC: OAuthWebVC = {
        let controller = OAuthWebVC()
        controller.view = NSView(frame: NSRect(x: 0, y: 0, width: 600, height: 400))
        controller.delegate = self
        controller.viewDidLoad()
        return controller
    }()

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

    @IBAction func register(_ sender: Any) {
        let _ = webVC.webView
        oauthswift = OAuth2Swift(consumerKey: "Client ID",
                                 consumerSecret: "Client Secret",
                                 authorizeUrl: "https://qiita.com/api/v2/oauth/authorize",
                                 accessTokenUrl: "https://qiita.com/api/v2/access_tokens",
                                 responseType: "code")
        oauthswift?.authorizeURLHandler = getURLHandler()
        oauthswift?.allowMissingStateCheck = true
        let _ = oauthswift?.authorize(
            withCallbackURL: URL(string: "oauth-qiita-station://oauth-callback")!,
            scope: "read_qiita write_qiita",
            state: "",
            headers: ["Content-Type" : "application/json"],
            completionHandler: { (result) in
                switch result {
                case .success(let (credential, _, _)):
                    Swift.print("token", credential.oauthToken)
                    self.client = OAuthSwiftClient(credential: credential)
                case .failure(let error):
                    Swift.print(error.description)
                }

        })
    }

    func getURLHandler() -> OAuthSwiftURLHandlerType {
        if webVC.parent == nil {
            self.presentAsSheet(webVC)
        }
        return webVC
    }

    @IBAction func getUserInfo(_ sender: Any) {
        let userID: String = "Kyome"
        client?.get(
            URL(string: "https://qiita.com/api/v2/users/\(userID)")!,
            completionHandler: { (result) in
                switch result {
                case .success(let response):
                    if let json = try? response.jsonObject(options: .allowFragments) as? [String : Any] {
                        Swift.print(json)
                    }
                case .failure(let error):
                    Swift.print(error.description)
                }
        })
    }

    func oauthWebViewControllerWillAppear() {
    }

    func oauthWebViewControllerDidAppear() {
    }

    func oauthWebViewControllerWillDisappear() {
    }

    func oauthWebViewControllerDidDisappear() {
        oauthswift?.cancel()
    }

}

参考

Qiita APIのOAuth認証は一筋縄ではいかない

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

ストーリーボードをつかわず、再生↔一時停止をToolbarで書くとこうなるサンプル(Playgroundサンプル付き)

はじめに

オーディオプレーヤーの再生↔一時停止のようにツールバーをつくるとき、みなさんはどのように書いていますか。:thinking:

  • Storyboardをつかわない
  • Swift4
  • NavigationControllerをつかう

こんな条件のときのサンプルが見つけられなかったので、いろいろためしながら実装した方法を紹介したいとおもいます。

実行結果は、こんな感じになります。
audio_playground.gif
この記事の最後に、Playgroundにコピペすると動作するコードを載せていますのでそちらも参考になれば:relaxed:

前準備

ビューの作成時に、self.toolbarItemsをセットしておく必要があります。
NavigationControllerにツールバーを表示させる方法は、NavigationControllerのToolbarを特定のViewControllerでのみ有効にする - Qiitaを参考にしてみてください。

ボタンをトグルするコード

ボタンのオブジェクトはひとつにしてbarButtonSystemItem.play.pauseでトグルすればいいのでは?とおもったのですが、再生ボタンと一時停止ボタンの2つのオブジェクトを用意して差し替えるのがベターな方法みたいです。

private var audioPlaying = false

// MARK: ツールバーのアイテム
private var uiPlayButtonItem : UIBarButtonItem {
  let button = UIBarButtonItem(
    barButtonSystemItem: .play,
    target: self,
    action: #selector(onButton(_:)))
  button.tag = 1
  return button
}
private var uiPauseButtonItem : UIBarButtonItem {
  let button = UIBarButtonItem(
    barButtonSystemItem: .pause,
    target: self,
    action: #selector(onButton(_:)))
  button.tag = 1 // 置換するのでtagはおなじ
  return button
}

private lazy var uiButtonItems: [UIBarButtonItem] = {

  let flexibleItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
  flexibleItem.tag = 0
  return [
    flexibleItem,
    uiPauseButtonItem, // 最初は再生
    flexibleItem,
  ]
}()

@objc func onButton(_ sender: UIBarButtonItem) {
  // 再生ボタンを差し替えるので、index位置を取得する
  guard let buttonIndex = self.uiButtonItems.firstIndex(
    where: {$0.tag == 1}
  ) else {
    print( "再生ボタンのインデックスが取得できてません")
    return
  }

  if self.audioPlaying {
    self.audioPlaying = false
    self.toolbarItems?[buttonIndex] = self.uiPlayButtonItem
    return
  }

  self.audioPlaying = true
  self.toolbarItems?[buttonIndex] = self.uiPauseButtonItem
}

Playgroundで試せるコード

image.png

以下を実行すると、冒頭のスクショのような動作になります。

import UIKit
import XCPlayground
import PlaygroundSupport

class ViewController: UIViewController {
    private var audioPlaying = false
    let label = UILabel()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        label.textAlignment = .center
        view.addSubview(label)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        label.frame = view.bounds
    }

    // ツールバーを表示する
    override func viewWillAppear(_ animated: Bool) {
        self.toolbarItems = self.uiButtonItems
        self.updateButton()
        self.updateLabel()
        self.navigationController?.setToolbarHidden(false, animated: false)
    }

    override func viewWillDisappear(_ animated: Bool) {
        self.navigationController?.setToolbarHidden(true, animated: false)
        super.viewWillDisappear(animated)
    }

    // MARK: ツールバーのアイテム
    private var uiPlayButtonItem : UIBarButtonItem {
      let button = UIBarButtonItem(
        barButtonSystemItem: .play,
        target: self,
        action: #selector(onButton(_:)))
      button.tag = 1
      return button
    }
    private var uiPauseButtonItem : UIBarButtonItem {
      let button = UIBarButtonItem(
        barButtonSystemItem: .pause,
        target: self,
        action: #selector(onButton(_:)))
      button.tag = 1 // 置換するのでtagはおなじ
      return button
    }

    private lazy var uiButtonItems: [UIBarButtonItem] = {

      let flexibleItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
      flexibleItem.tag = 0
      return [
        flexibleItem,
        uiPauseButtonItem, // 最初は再生
        flexibleItem,
      ]
    }()

    func updateLabel() {
        label.text = self.audioPlaying ? "playing..." : "pause"
    }

    func togglePlayState() {
        self.audioPlaying = !self.audioPlaying
    }

    func updateButton() {
        // 再生ボタンを差し替えるので、index位置を取得する
        guard let buttonIndex = self.uiButtonItems.firstIndex(
          where: {$0.tag == 1}
        ) else {
          print( "再生ボタンのインデックスが取得できてません")
          return
        }
        if self.audioPlaying {
          self.toolbarItems?[buttonIndex] = self.uiPauseButtonItem
          return
        }
        self.toolbarItems?[buttonIndex] = self.uiPlayButtonItem
    }

    @objc func onButton(_ sender: UIBarButtonItem) {
        self.togglePlayState()
        self.updateButton()
        self.updateLabel()
    }
}

var ctrl = ViewController()
let navigationController = UINavigationController(rootViewController: ctrl)
navigationController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = navigationController.view
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NavigationControllerのToolbarを特定のViewControllerでのみ有効にする

UIViewController
private lazy var uiButtonItems: [UIBarButtonItem] = {
  // 適当なUIBarButtonItemの配列
  return [...]
}()

// ビュー表示のタイミングでツールバーを表示する
override func viewWillAppear(_ animated: Bool) {
  self.toolbarItems = uiButtonItems
  self.navigationController?.setToolbarHidden(false, animated: false)
}

// ビュー非表示のタイミングで非表示に戻す
override func viewWillDisappear(_ animated: Bool) {
  self.navigationController?.setToolbarHidden(true, animated: false)
  super.viewWillDisappear(animated)    
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift 配列から条件にあった要素のインデックス値を取得する

String配列の一部を置換する例

var students = ["マリオ", "ピーチ", "ワルイージ", "ヨッシー"]
if let i = students.firstIndex(of: "ワルイージ") {
    students[i] = "ルイージ"
}
print(students)
// Prints "["マリオ", "ピーチ", "ルイージ", "ヨッシー"]"

UIBarButtonItem配列でtag==3な要素のインデックスを取得する

let buttonA : UIBarButtonItem {
  let button = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(onPlayPauseButton(_:)))
  button.tag = 1
  return button
}

let buttonB : UIBarButtonItem {
  let button = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(onPlayPauseButton(_:)))
  button.tag = 3
  return button
}

// buttonB のインデックス位置を取得したい
let buttons = [ buttonA, buttonB ]
if let i = buttons.firstIndex(where: {$0.tag == 3}) else{
  print(i)
  // 1
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcodeでデバッグ中に変数の値が確認できなかったけど簡単に解決したメモ

そこにある変数が見えない

image.png
確かにそこにいるんだ。
folders という配列が…

image.png
しかし、見えないんだ…

(lldb) po folders
error: <EXPR>:3:1: error: use of unresolved identifier 'folders'
folders
^~~~~~~

(lldb) po self.folders
error: <EXPR>:3:1: error: use of unresolved identifier 'self'
self.folders
^~~~

(lldb) p folders
error: <EXPR>:3:1: error: use of unresolved identifier 'folders'
folders
^~~~~~~

値がどこからも確認できないんだ…

解決方法

ということで、SwiftとObjective-Cが混在しているプロジェクトで起きがちという噂の
「Xcodeでデバッグ中に変数の値が確認できない」
「lldbデバッグで変数の中身を見ようと、poしても見れない」
とお困りの方に朗報
こんなとき、コードにprint関数を埋め込んだりしてましたが、

po print(対象の変数)

(lldb) po print(folders)
["新しいフォルダ", "新しいフォルダ1", "新しいフォルダ2"]

lldbコマンドで、print関数の戻り値を print object するという方法で、コードを汚さず値を確認することができます。(できる場合があります。くらいにしておこうかな)

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

NeumorphismなUIをSwiftUIで作れるライブラリ、Neumorphismicを作ってみた

先日Neumorphism: 令和時代のスキューモーフィズムを読みました。

これが流行る頃にはSwiftUIも使えるようになってるだろう…ということでSwiftUIでNeumorphismのライブラリを作ってみました。Switchなどは作っていません。今後時間があれば作っていきたいですが、まだ形がコレと定まってもいないので難しそうですね。そもそも流行るのかもわかりませんし。

スターつけてくださると嬉しいです!

完成形

Demo view
Settings view

ModifierでShadowを実装する

Modifierについてはこの記事でひと通りわかると思います。簡単にいうと.fontとか.frameとかをまとめてViewに適合できるやつです。Buttonなどの場合はButtonStyleなどが使えればいいのですが、Appleさんが公開していないので諦めます。
SwiftUIでもshodowは1つしか追加できません。なのでZStackViewを2つ重ねてそれぞれに.shadowをつける必要があります。

struct ConvexModifier: ViewModifier {
    let lightShadowColor: Color
    let darkShadowColor: Color
    func body(content: Content) -> some View {
        ZStack {
            content
                .shadow(color: darkShadowColor, radius: 16, x: 9, y: 9)
            content
                .shadow(color: lightShadowColor, radius: 16, x: -9, y: -9)
        }
    }
}

しかし、これだけでは影の色をいちいち入力する必要があります。

色をEnvironmentで伝搬する

SwiftUIでは@Environmentを使うことでその子View全てに値を伝えることができます。詳しくはこの記事を読んでください。
そしてこれは自作することもできます。Neumorphismでは基本的にViewに1色しか使わないためこれが非常に有効です。さらに、ConvexModifierbaseColorを基準に影の色を決めればよくなります。自作する方法はこちらをご覧ください。

struct BaseColorKey: EnvironmentKey {
    static let defaultValue: Color = .gray
}

extension EnvironmentValues {
    var baseColor: Color {
        get { self[BaseColorKey.self] }
        set { self[BaseColorKey.self] = newValue }
    }
}

さて、色を変換したいわけだけど…

パッと見、SwiftUIのColorからはrgbやhueや取得できそうにありません。が、優しきAppleさんは.descriptionを用意してくれていました。#C1D2EBFFのようにカラーコードを返してくれます。ということでカラーコードからColorを生成(このリンクではUIColor)できるようにし、RGBとHSLRGBとHSBの変換コードを用意します。

Colorから色情報を取れるとわかったので、早速lighterColorを実装します。neumorphismPrimary(value:)は下に出てくるFloatingTabViewのラベルなどで使われています。

func getHSLA() -> (h: Double, s: Double, l: Double, a: Double) {
    let string = String(self.description.dropFirst())
    let v = Int(string, radix: 16) ?? 0

    let r = Double(v / Int(powf(256, 3)) % 256) / 255
    let g = Double(v / Int(powf(256, 2)) % 256) / 255
    let b = Double(v / Int(powf(256, 1)) % 256) / 255
    let a = Double(v / Int(powf(256, 0)) % 256) / 255

    let (h, s, l) = ColorTransformer.rgbToHsl(r: r, g: g, b: b)
    return (h, s, l, a)
}

func lighter(value: Double) -> Color {
    let (h, s, l, a) = getHSLA()
    let hsb = ColorTransformer.hslToHsb(h: h, s: s, l: l + value)
    return Color(hue: hsb.h, saturation: hsb.s, brightness: hsb.b, opacity: a)
}

func darker(value: Double) -> Color {
    let (h, s, l, a) = getHSLA()
    let hsb = ColorTransformer.hslToHsb(h: h, s: s, l: l - value)
    return Color(hue: hsb.h, saturation: hsb.s, brightness: hsb.b, opacity: a)
}

func primary(value: Double) -> Color {
    let (_, _, l, _) = getHSLA()
    return (l > 0.5) ? darker(value: value) : lighter(value: value)
}
struct ColorExtension_Previews: PreviewProvider {
    static var previews: some View {
        let color = Color(hex: "C1D2EB")

        return Group {
            ColorPreview(color)
            ColorPreview(color.lighter(value: 0.12))
            ColorPreview(color.darker(value: 0.18))
        }
        .previewLayout(.fixed(width: 200, height: 100))
    }
}

Colors
こうなりました。影として使わないとよくわかりませんね。

ConvexModifierを完成させる

材料は揃ったので合わせてみましょう。

struct ConvexModifier: ViewModifier {

    @Environment(\.baseColor) var baseColor: Color

    func body(content: Content) -> some View {
        ZStack {
            content
                .shadow(color: baseColor.darkerColor(value: 0.18), radius: 16, x: 9, y: 9)
            content
                .shadow(color: baseColor.lighterColor(value: 0.12), radius: 16, x: -9, y: -9)
        }
    }
}
struct ConvexModifier_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color(hex: "C1D2EB")
                .edgesIgnoringSafeArea(.all)

            Circle()
                .fill(Color(hex: "C1D2EB"))
                .modifier(ConvexModifier())
                .frame(width: 300, height: 300)
        }
        .environment(\.baseColor, Color(hex: "C1D2EB"))
    }
}

.environmentbaseColorを伝えるの忘れないようにしましょう。
Image
いい感じですね!

environmentを使ったので当然

struct ConvexModifier_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color(hex: "C1D2EB")
                .edgesIgnoringSafeArea(.all)

            Circle()
                .fill(Color(hex: "C1D2EB"))
                .frame(width: 300, height: 300)
                .modifier(ConvexModifier())
                // `environment`で影の色を変える
                .environment(\.baseColor, Color.red)
        }
        .environment(\.baseColor, Color(hex: "C1D2EB"))
    }
}

とできるだろうと思っていたのですができませんでした。.redの時は黒い影が出てきて、その他の場合は影がなくなりました。Color(hex:)を使ったら行けたのでよくわかりません。

Highlight時に表示を変えられるButtonも作ってみた

ハイライト時にNeumorphismではどうするのが正解なんだろうと考えるためにとりあえず作ってみたのですが、標準のButtonでもいい気がします。個人的に標準Buttonは ハイライト時に色が薄すぎる気がするのでこれを使ってます。押下時にボタンを小さくしたいときなどに使ってみてください。

struct HighlightableButton<Label>: View where Label: View {

    private let action: () -> Void
    private let label: (Bool) -> Label

    public init(
        action: @escaping () -> Void,
        label: @escaping (Bool) -> Label
    ) {
        self.action = action
        self.label = label
    }

    @State private var isHighlighted = false

    public var body: some View {
        label(isHighlighted)
            .animation(.easeOut(duration: 0.05))
            .gesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in
                    withAnimation { self.isHighlighted = true }
                }
                .onEnded { _ in
                    self.action()
                    withAnimation { self.isHighlighted = false }
                }
        )
    }
}
struct ConvexModifier_HighlightableButton_ForPreviews: View {
    @State var isSelected = false
    var body: some View {
        HighlightableButton(action: {
            self.isSelected.toggle()
        }) { isH in
            Image(systemName: self.isSelected ? "house.fill" : "house")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: isH ? 54 : 60)
                .foregroundColor(Color(hex: "C1D2EB").darker(value: 0.18))
                .background(
                    Circle()
                        .fill(Color(hex: "C1D2EB"))
                        .frame(width: isH ? 90 : 100,
                               height: isH ? 90 : 100)
                        .modifier(ConvexModifier())
                )
                .opacity(isH ? 0.6 : 1)
        }
    }
}

.modifier(ConvexModifier())Buttonの中に書いてください。外に書くときちんと適用されません。ForPreviewsみたいな名前になっているのは、PreviewProviderの中には@Stateが置けないからです。
GIF

FloatingTabViewも作ってみた

@touyouもタブを作ってることだし作るかーみたいな感じで作ってみました。ViewBuilderを読み解こうとまでは思わなかったので、標準のTabViewのような綺麗さはありませんが、まあ使えるのではないでしょうか。タブの数は4つまでにしてみました。その他を実装する気力はなかったので切り捨てています。あとで多分何とかします。
使い方はこんな感じです。

struct FloatingTabView_ForPreviews: View {
    enum Season: String, CaseIterable {
        case spring, summer, fall, winter
        var color: Color {
            switch self {
            case .spring: return .pink
            case .summer: return .blue
            case .fall:   return .orange
            case .winter: return .white
            }
        }
    }
    @State var season: Season = .spring
    var body: some View {
        FloatingTabView(selection: $season, labelText: { s in
            s.rawValue
        }, labelImage: { _ in Image(systemName: "camera") }) { s in
            s.color.edgesIgnoringSafeArea(.all)
        }
    }
}

例を作るのが面倒なのが目に見えますね…。labelImageなんてカメラだけですし。この例では色を変えていますが、Neumorphismでは色を変えるのはご法度なので注意。(もちろん局所的にアクセントとして使うのはOKです)
Screen Shot 2020-01-12 at 20.34.46.png
ああ、影の色が汚い…。まあ色変えるとこうなるよっていう悪い例と思ってください。
ちなみにGeometryReaderのせいかLive PreviewにしていないとTabが下に落ちてしまいました。SwiftUIは7不思議どころじゃありません。そこら辺に穴が一杯です。全く理由がわからず何となくLive Previewにしてみたところ正しいことがわかりました。恐ろしや。

凹も作りたかったけど

凹凸どちらも作りたかったのですが、凹の方はいい案が思い浮かばず、凸だけになってしまいました。適合したいaViewよりひとまわり大きいbViewを作って、そこからaViewの大きさを切り抜いて、aViewの上にbViewを2つ置いて影をつければ行けそうだなとは思ったんですが、くり抜く方法がわかりませんでした。Pathを使えば何とかなりそうですが、Viewの形を取る方法もないですし…。
あと、SwiftUI製ですし、全プラットフォームに対応させたかったのですが、macOSにSF Symbolsがなかったり、tvOSにDragGestureがなかったりと面倒になってやめました() まあそこまで大変そうでもないのでいつかします。

使う上での諸注意

HighlightableButtonのところでも書きましたが、Buttonで使う際は.modifier(ConvexModifier())Buttonの中に書いてください。そうしないときれいに作れません。
またBinding系のコンポーネントは2つ同時にViewに存在すると使えなくなるようです。

TextField("C1D2EB", text: $model.userInput)
    .foregroundColor(baseColor.nmPrimary(value: 1))
    .padding(5)
    .background(
        RoundedRectangle(cornerRadius: 5, style: .continuous)
            .fill(baseColor)
            .modifier(NMConvexModifier(radius: 9))
    )

のようにbackgroundに設定したView.modifier(ConvexModifier())してください。Viewの量的にもこちらの方がパフォーマンスもいいです。

初OSS & 初Qiita?

SwiftUIなのでマルチプラットフォームに対応のOSSにしたかったわけですが、iOSのみとは作り方が違うようで。WWDCを参考に作らせていただきました。
Qiitaも初投稿ですが、そろそろ開発から離れて受験勉強しないと浪人する未来しか見えないので当分記事を書くことはないでしょう。1年後に戻って来れるように頑張ります。

いいねとスターつけてくださると嬉しいです!

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

[Swift]?1ヶ月に使うトイレットペーパーの数を計算したい?

僕は快便なのでうんち?をよくします。調子が良い時だと12ロール入りを3週間未満で使い切ります?

そこで今回はSwiftを使って1ヶ月に使うトイレットペーパーの数を動的に計算したいと思います?
(ションベンは無いものとする)

開発環境

macOS Catalina v.10.15.1
Xcode v.11.3
Swift v.5.1.3

?Playground

計算するだけなので、Playgroundを使います。

まず、1ロールの長さと1ヶ月を定義します?

// 1ロールの長さはJIS規格で決まっているらしいです。今回は一般的な32.5mで計算します
let length: Float = 32.5
// 1ヶ月を31日とする
let month: Int = 31

そして1日にうんこをする数を代入する変数をget,setプロパティで実装します!?

var PoopCount: Int {
    get {
        // 日本人が1回のトイレで使うトイレットペーパーの平均の長さ146cm(ググりました)
        let aboutOnceUse: Float = 1.46
        // 1ロールでうんこできる回数
        let oneRollOfPoop = length / aboutOnceUse
        // 22回うんこできる
        return Int(oneRollOfPoop)
    }
    set {
        // 1日で使うロールの長さ
        let daysOfRollLength = Float(newValue) * 1.46
        // 1ロールでうんこできる日数
        let oneRollOfDays = length / daysOfRollLength
        // 1ヶ月に必要なトイレットペーパー
        let totalRoll = month / Int(oneRollOfDays)
        // 4ロール必要
        print("\(totalRoll)")
    }
}
// 1日3回うんこすると...
PoopCount = 3

これで1ヶ月に使うトイレットペーパーの数を計算出来ました!
それでは、getとsetプロパティについて説明します?‍?

?get

get {
        // 日本人が1回のトイレで使うトイレットペーパーの平均の長さ146cm(ググりました)
        let aboutOnceUse: Float = 1.46
        // 1ロールでうんこできる回数
        let oneRollOfPoop = length / aboutOnceUse
        // 22回うんこできる
        return Int(oneRollOfPoop)
    }

これは変数が実行される度にgetの処理が実行され、実行結果が返ってきます。
今回の場合だと、PoopCountがInt型なので、Int型にキャストして返してます。

getは読み取り専用なので、今回のような指定した値から計算するだけなら必要ないです、ただ使いたかったので1ロールでうんこできる処理を書きました。?

?set

set {
        // 1日で使うロールの長さ
        let daysOfRollLength = Float(newValue) * 1.46
        // 1ロールでうんこできる日数
        let oneRollOfDays = length / daysOfRollLength
        // 1ヶ月に必要なトイレットペーパー
        let totalRoll = month / Int(oneRollOfDays)
        // 4ロール必要
        print("\(totalRoll)")
    }
}
// 1日3回うんこすると...
PoopCount = 3

今回のメイン処理となります!
これは変数に代入された値を元に計算を始める処理を書きます!
じゃあその代入した値はどこいくねん??ってなるかと思いますが、setプロパティを使ってる変数に代入した値はnewValueに代入されます!

こういう意味です?

var PoopCount: Int {
    get {
        // 日本人が1回のトイレで使うトイレットペーパーの平均の長さ146cm(ググりました)
        let aboutOnceUse: Float = 1.46
        // 1ロールでうんこできる回数
        let oneRollOfPoop = length / aboutOnceUse
        // 22回うんこできる
        return Int(oneRollOfPoop)
    }
    set {
        // 1日で使うロールの長さ           ?ここに来る
        let daysOfRollLength = Float(newValue) * 1.46
        // 1ロールでうんこできる日数
        let oneRollOfDays = length / daysOfRollLength
        // 1ヶ月に必要なトイレットペーパー
        let totalRoll = month / Int(oneRollOfDays)
        // 4ロール必要
        print("\(totalRoll)")
    }
}
// 1日3回うんこすると...
PoopCount = 3    ?この値が

?おまけ

他にもwillSetやdidSetを使って値の監視を出来ます?

?willset

willsetは元ある値よりも代入された値に対して処理を書くことが出来ます。setとほぼ一緒ですね?‍♂️

こんな感じ?

var PoopCount: Int = 1 {
    willSet {
        if newValue < 1 {
            print("便秘です、食物繊維を摂取しましょう?")
        }
    }
}
// 1日にうんこする回数?
PoopCount = 0

?didSet

didSetは代入された値よりも元ある値で処理を書くことが出来ます。
例えば変数の値が動的に変わるとします。その値はDBに保存されていて、ユーザーが前日入力した値が反映される実装をしていると仮定しましょう。
元ある値はoldValueに代入されます。

こんな感じ?

var poop = DBの値を読み込ませる

var PoopCount: Int = poop {
    willSet {
        if newValue < 1 {
            print("便秘です、食物繊維を摂取しましょう?")
        } else if newValue > 10 {
            print("病院行きましょう?")
        } else {
            print("良い感じ!!?")
        }
    }
    didSet { ?ここに前日のDBの値が反映される
        if oldValue > 10 {
            print("病院行きましたか?")
        } else if oldValue < 1 {
            print("昨日はうんちしてません?")
        } else {
            print("快便や?")
        }
    }
}

PoopCount = 2

こんな感じで前日と比較したうんち記録を作成することが出来ます?

?まとめ?

どうでしたでしょうか?文章を読むだけだと理解しずらいと思うので、実際にPlaygroundで試してみてください!
きっと理解できるはずです?

それにしても、一人暮らしだとトイレットペーパーってあまり使わないんですね、俺はトイレットペーパー を使いすぎなのかな笑

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

SwiftUI 実践 tips 集

はじめに

MIHO というキャラボイスアプリを SwiftUI と Combine を用いて開発しました。互換性の欄が iOS13 以降となっていることが確認できるかと思います(証拠とは言えませんが)。
このアプリを開発するにあたって学んだことをここにまとめたいと思います。

TapGesture & ContentShape

List を用いた UI で、セルをタップすると別の View に遷移する処理は公式のチュートリアルでも確認できます。非常にシンプルです。
では、「セルをタップしたときに音声を再生させる」のように、セルをタップしたときに独自の処理を走らせる場合はどうしたら良いでしょうか。解決方法としては Button を使う方法と onTapGesture を使う方法が考えられます。
今回は onTapGesture を使う場合を紹介します。

List(VoiceModel.allCases) { item in
    VoiceRowView(voice: item)
        .onTapGesture {
            viewModel.action()
        }
}

使い方は上記の通り、非常にシンプルです。

struct VoiceRowView: View {
    var body: some View {
        HStack(spacing: 8) {
            Image("image") 
            Text("Apple")
            Spacer()
        }
    }
}

次に、そのセルのレイアウトが上記のように Spacer を用いた実装になっていたとします。
この場合、左から画像・文字列と並び、右側には空白ができることになります(もちろん文字数が多い場合は右端まで埋まる)。ここで勘がいい方は気がつくかもしれませんが、この右側の空白部分ではタップ反応を受け取ることができません。

List(VoiceModel.allCases) { item in
    VoiceRowView(voice: item)
        .contentShape(Rectangle())
        .onTapGesture {
            self.playView.viewModel.prepare(item: item)
        }
}

onTapGesture を使うという条件下で、この問題の解決方法は上記のようになります。 .contentShape(Rectangle()) と指定することでセルの形を長方形であると定義します。こうすることによって空白があったとしてもセル全体でアクションを受け取ることが可能になります。

参考文献:
How to read tap and double-tap gestures
SwiftUI can't tap in Spacer of HStack

GeometryReader

View のサイズを画面サイズから計算して指定したい場合があると思います。今回は、 16:9 の View を作成する例とともにご紹介します。

var body: some View {
    GeometryReader { geometry in
        AVPlayerView(bundleDataName: "header")
            .frame(width: geometry.size.width, height: (9 * geometry.size.width) / 16)
    }
}

上記が UI 設計例になります。 GeometryReader を用いることで その View の width と height を取得することが可能です。 ただ、これが端末の画面サイズを示すものではないことに注意してください(Safe Area のことも意識してあげてくださいね)。

参考文献:
How to provide relative sizes using GeometryReader

Sheet & Alert

画面遷移でプッシュ遷移は List を用いることでシンプルに実装が可能です。もう一つの遷移方法として存在する「モーダル遷移」を実現できるのが sheet です。ここでは、先程も説明した TapGesture とともに実装する例でご紹介します。

@State private var isShowDetail: Bool = false

var body: some View {
    GeometryReader { geometry in
        ...
    }
    .onTapGesture {
        self.isShowDetail = true
    }
    .sheet(isPresented: $isShowDetail) {
        VoiceDetailView()
    }
}

様々な実装パターンが考えられますが、上記のような処理でモーダル遷移が実現できます。仕組みとしては、まず画面遷移を行うかどうかを示すフラグを宣言しておき、 ButtonTapGesture によるアクションキャッチ時にフラグを書き換えてあげます。そして、そのフラグの変化を検知した sheet が画面遷移を行うといった動きになります。

@State private var isShowAlert: Bool = false

var body: some View {
    GeometryReader { geometry in
        ...
    }
    .onTapGesture {
        self.isShowAlert = true
    }
    .alert(isPresented: $isShowAlert) {
        Alert(
            title: Text("Title"),
            message: Text("message"),
            dismissButton: .default(Text("OK"))
        )
    }
}

ついでに Alert の表示方法も同じ考え方なので載せておきます。非常にシンプルですね。

*Representable

私の GitHub や Qiita 記事、勉強会資料などで何度も登場してきた *Representable ですが、今回はちょっと実践的な内容となります。
今回は SwiftUI にはないパーツである UIProgressView を SwiftUI に対応させる例でご紹介します。

struct ProgressView: UIViewRepresentable {
    @Binding var progress: Float
    var progressTintColor: UIColor

    func makeUIView(context: Context) -> UIProgressView {
        return UIProgressView().apply { this in
            this.progressTintColor = progressTintColor
        }
    }

    func updateUIView(_ uiView: UIProgressView, context: Context) {
        uiView.setProgress(progress, animated: true)
    }
}

これが実装例になります。 UIProgressViewprogress の値を変化させてあげなければいけません。そのため、単純に UIViewRepresentable に準拠させてあげるだけでは動作してくれません。
そこで、 @Binding を用いて外部の値変化を監視できるようにしています。

#if DEBUG
struct ProgressViewPreviews: PreviewProvider {
    static var previews: some View {
        ProgressView(progress: .constant(0.5), progressTintColor: .blue)
            .previewLayout(.fixed(width: 300, height: 10))
    }
}
#endif

ただ、先程作成した ProgressView をプレビュー可能にするとき、困ることがあります。 @Binding var progress: Float に渡す初期値です。対応方法は上記実装例にもあるように .constant() に値をセットしたものを指定するという感じになります。

参考文献:
How to use UIKit in SwiftUI
SwiftUI @Binding Initialize

さいごに

説明間違えやアドバイス、ご指摘などありましたら遠慮なくコメントいただけると嬉しいです!
最後まで読んでいただき、ありがとうございます!!

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