20190929のSwiftに関する記事は10件です。

SwiftUIにおけるscaledToFill()とかscaledToFit()とかaspectRatio(_:contentMode:)等の使い分け

主にImageのリサイズの時に、FillとかFitとかaspectRatioとか色々あって何を使えばよかったんだっけ?となるので備忘録としてまとめました。

そもそも…

Fillとは?

枠内いっぱいに表示されるように拡大縮小されるので、はみ出した分の左右もしくは上下は切り取られる。

Fitとは?

枠内からハミ出さないように拡大縮小されるので、左右もしくは上下に空白ができる可能性がある。

SwiftUIにおけるメソッドの違いは?

scaledToFill()

使用例
Image("SampleImage")
    .resizable()
    .scaledToFill()

scaledToFill()の公式ドキュメント
アスペクト比を維持しながら、親に合わせてこのビューをスケーリング
枠全体を覆うように拡大縮小されるので、はみ出した分の左右もしくは上下が切り取られる可能性がある。
aspectRatio(nil, contentMode: .fill) と同一効果。

scaledToFit()

使用例
Image("SampleImage")
    .resizable()
    .scaledToFit()

scaledToFit()の公式ドキュメント
アスペクト比を維持しながら、親に合わせてこのビューをスケーリング
全体が表示されるように拡大縮小されるので、左右もしくは上下に空白ができる可能性がある。
aspectRatio(nil, contentMode: .fit) と同一効果。

aspectRatio(_:contentMode:)

使用例
Image("SampleImage")
    .resizable()
    .aspectRatio(0.5, contentMode: .fit)

aspectRatio(_:contentMode:)の公式ドキュメント
第一引数で、幅と高さの比率を指定する。 (e.g. 0.75)
contentMode では、fit or fill を選択する。

以上、他にまた類似の指定方法があって迷いそうだったら追加したいです。

間違い等あれば教えていただければ嬉しいです!

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

Swiftでルビを表示させたい件について

どうでもいいこと

先日iOSDCに参加して以降ためこんでいた発信欲をそろそろ解放したいと思い、例によって備忘録でしかない内容をiOSエンジニア初心者の自分のために書きます。(ぜひ来年はLT枠とかで登壇したい...!)

Swiftでルビを表示させたい初心者へ

今回は仕事で少し触れそうなので、ルビ表示をSwiftで実装する、というテーマです。
ほとんど自分のためとはいえ、私のような初心者のためになることもあるのではという前向きな気持ちも抱きながら書いています。
以下の2つの記事を全面的に参考にさせていただきましたが、こちらで簡潔にまとめられている説明を、わざわざくどく、面倒くさく、まどろっこしく初心者向けにしたものがこの記事です。
(ルビ表示の実装が初心者に需要ある?という自分へのツッコミをスルーしながら書いてます。)

実装したコードがこちら

実装の主な方針は以下の通りです。

  • 前提として
    • 環境:Xcode10.3、Swift5
    • 青空文庫の表記(「紅玉《ルビー》」)をお借りしてルビ付文字のStringを表現
  • 正規表現を使ってString内のルビ付文字を特定 → StringExtension.swiftを参照
  • CTRubyAnnotationとNSAttributedStringを使ってルビを生成 → StringExtension.swiftを参照
  • カスタムUILabelクラスを作ってルビを表示 → RubyLabel.swiftとRubyViewController.swiftを参照

ではさっそく、正規表現を使ってルビ付文字を特定し、ルビを生成する実装です。

StringExtension.swift
import UIKit

extension String {
    // 文字列の範囲
    private var stringRange: NSRange {
        return NSMakeRange(0, self.utf16.count)
    }

    // 特定の正規表現を検索
    private func searchRegex(of pattern: String) -> NSTextCheckingResult? {
        do {
            let patternToSearch = try NSRegularExpression(pattern: pattern)
            return patternToSearch.firstMatch(in: self, range: stringRange)
        } catch { return nil }
    }

    // 特定の正規表現を置換
    private func replaceRegex(of pattern: String, with templete: String) -> String {
        do {
            let patternToReplace = try NSRegularExpression(pattern: pattern)
            return patternToReplace.stringByReplacingMatches(in: self, range: stringRange, withTemplate: templete)
        } catch { return self }
    }

    // ルビを生成
    func createRuby() -> NSMutableAttributedString {
        let textWithRuby = self
            // ルビ付文字(「|紅玉《ルビー》」)を特定し文字列を分割
            .replaceRegex(of: "(|.+?《.+?》)", with: ",$1,")
            .components(separatedBy: ",")
            // ルビ付文字のルビを設定
            .map { component -> NSAttributedString in
                // ベース文字(漢字など)とルビをそれぞれ取得
                guard let pair = component.searchRegex(of: "|(.+?)《(.+?)》") else {
                    return NSAttributedString(string: component)
                }
                let component = component as NSString
                let baseText = component.substring(with: pair.range(at: 1))
                let rubyText = component.substring(with: pair.range(at: 2))

                // ルビの表示に関する設定
                let rubyAttribute: [CFString: Any] =  [
                    kCTRubyAnnotationSizeFactorAttributeName: 0.5,
                    kCTForegroundColorAttributeName: UIColor.darkGray
                ]
                let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(
                    .auto, .auto, .before, rubyText as CFString, rubyAttribute as CFDictionary
                )

                return NSAttributedString(string: baseText,
                                          attributes: [kCTRubyAnnotationAttributeName as NSAttributedString.Key: rubyAnnotation])
            }
            // 分割されていた文字列を結合
            .reduce(NSMutableAttributedString()) { $0.append($1); return $0 }
        return textWithRuby
    }
}

ルビの表示に関する設定で出てきたコードに説明を加えます。

let rubyAttribute: [CFString : Any] =  [
    kCTRubyAnnotationSizeFactorAttributeName: // ベース文字(漢字など)に対するルビサイズの割合
    kCTForegroundColorAttributeName: // ルビの色
]
let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(
    alignment: /* ベース文字に対するルビの整列形式を指定
               .auto = CoreTextが調整?(よくわかってないです...上手くやってくれるっぽい)
               .start = ベース文字と前を揃えて配置
               .center = ベース文字と中心を揃えて配置
               .end = ベース文字と後ろを揃えて配置
               .distributeLetter = ベース文字と前と後ろが揃うように均等配置
               .distributeSpace = ルビの文字間が均等になるように配置
               .lineEdge = ベース文字と前を揃えて文字間を詰めて配置 */
    overhang: /* ベース文字よりルビが長い場合のルビのはみ出し可否を指定
               .auto = ベース文字の前後へはみ出し可能
               .start = ベース文字の前へのはみ出しのみ可能
               .end = ベース文字の後ろへのはみ出しのみ可能
               .none = ベース文字からのはみ出し不可 */
    position: /* ベース文字に対するルビの位置を指定
               .before = ベース文字の上にルビを横表示
               .after = ベース文字の下にルビを横表示
               .inLine = ベース文字の後ろにルビを横表示
               .interCharacter = ベース文字の後ろにルビを縦表示 */
    string: // 表示するルビの文字列
    attributes: // 表示するルビの文字列に設定済みのattributes
)

これでルビの生成は終了です。

最後にUILabelをカスタムしてルビを表示させる実装です。

RubyLabel.swift
import UIKit

class RubyLabel: UILabel {
    // ルビを表示
    override func draw(_ rect: CGRect) {
        // 描画位置を設定
        guard let context = UIGraphicsGetCurrentContext() else { return }
        context.translateBy(x: 0, y: rect.height)
        context.scaleBy(x: 1.0, y: -1.0)

        // ルビを挿入
        guard let attributedText = self.attributedText else { return }
        let frame = CTFramesetterCreateFrame(
            CTFramesetterCreateWithAttributedString(attributedText),
            CFRangeMake(0, attributedText.length),
            CGPath(rect: rect, transform: nil),
            nil)

        // 描画に反映
        CTFrameDraw(frame, context)
    }
}

(グラフィックスコンテキスト周りの理解はまだまだ浅い...)
ルビ表示が途切れてしまう場合はUILabelのサイズを調整するなどします。

ルビが表示されました!

スクリーンショット 2019-09-29 19.02.43.png

参考までにViewController(UILabelは上記のカスタムクラスを継承)はこんな感じにしています。

RubyViewController.swift
import UIKit

class RubyViewController: UIViewController {
    @IBOutlet var rubyLabel: RubyLabel!

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

    private func setUpLabel() {
        rubyLabel.attributedText = "|紅玉《ルビー》がほしい".createRuby()
        rubyLabel.textAlignment = .center
        rubyLabel.font = .systemFont(ofSize: 30.0)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS13のPageSheetでdismissを呼んだ時にもpresentationControllerDidDismissを呼ぶ話

はじめに

こんにちは、fummicc1です。今回は、iOS13から変更されたmodalPresentationStyle.pageSheetを設定した場合の挙動を説明した後に、実際にどういうコードを書くのがいいのかを紹介します。

iOS13のページシート

iOS13からiPhoneでページシートの挙動が大きく変わりました。そして、デフォルトでページシートで遷移されるようになりました。
今まで(iOS13以前)は.fullScreenと同じ挙動だったのですが、今回から被さるようなUIになりました。(画像参照)
Simulator Screen Shot - iPhone 11 Pro Max - 2019-09-29 at 17.01.29.png

まず、これによって以下のようなAPIが呼ばれなくなりました。

  • viewWillAppear
  • viewWillDisappear

これは既存のアプリケーションを破壊する可能性があるので至急対応しているところが多いと思います。
例えば、model遷移先の画面で何か日記やToDoを作成し、saveボタンを押して保存処理をし画面をdismissします。
その場合、従来はviewWillAppearcollectionViewtableViewなどのUIの更新処理をかけている場合多いです。

しかし、pageSheetによる遷移ではdismissしてもviewWillAppearは呼ばれないので、日記やToDoを保存した後の状態がUIに反映されません。今回は、これに対応する方法を紹介します。

対応策

1. .fullScreenにする。

modalPresentationStyle.pageSheetではなく、.fullScreenを指定します。そうすれば既存の手法で話を進められます。

2. .pageSheetのままで実装する。

presentationControllerDidDismissというメソッドがiOS13から追加されているので、それを使用します。

このメソッドは遷移先の画面のインスタンスでpresentationController?.delegate = selfを実装します。このとき、self(遷移元の画面)UIAdaptivePresentationControllerDelegateに準拠している必要があります。

また、このUIAdaptivePresentationControllerDelegatepresentationControllerDidDismissを所持しているので以下のように実装します。

RootViewController.swift
func transitionToModalViewController() {
    let modalViewController = storyboard?.instantiateViewController(identifier: "ModalViewController") as! ModalViewController
    modalViewController.presentationController?.delegate = self // ここがポイント
    present(modalViewController, animated: true, completion: nil)
}
RootViewController.swift
extension RootViewController: UIAdaptivePresentationControllerDelegate {
    // このメソッドを実装
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        // Modal画面から戻った際の画面の更新処理を行う。 (collectionView.reloadDataなど。)
    }
}

これで、ユーザーがModal画面で下からスワイプをしたときにも更新処理ができます。

dismissでModalを閉じるとpresentationControllerDidDismissが呼ばれない。

これの対処法は、ベストプラクティスではないような気もしますが、遷移作の画面でdismissをオーバーライドし、その中でpresentationController.delegate?.presentationControllerDidDismiss?(presentationController)を呼びます。

ModalViewController.swift
extension ModalViewController {   
    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        super.dismiss(animated: flag, completion: completion)
        guard let presentationController = presentationController else {
            return
        }
        presentationController.delegate?.presentationControllerDidDismiss?(presentationController)
    }
}

これでdismissで画面を戻ってもUIの更新処理が呼ばれます。

最後に

iOS13の変化として、ダークモード対応やSign in with Apple対応などでも大変なのですが、Modalも結構変化して対応するのにコストがかかるなと感じていて、今回の記事も少ないリソースの中での解決策で個人的には満足した実装ではないので、今後APIを改善するなり、もっといい方法があるといいなと思いっています。
修正・意見があればコメントお待ちしています。参考になれば幸いです!

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

【swift】chartsを使って体重管理アプリを作ってみた

入力値がグラフに反映される実装をしてみました

流れとしてはこんな感じ

初期画面で体重と体脂肪率を入力⇨登録ボタンで画面遷移、その日の情報が登録されてグラフに表示、グラフタップでその日の情報が表示されるって感じです。

コードメインになります。

メイン画面の方はこんな感じです。

main
import UIKit

class HomeViewController: UIViewController,UITextFieldDelegate,UITableViewDelegate,UITableViewDataSource {

    @IBOutlet weak var weightLabel: UITextField!
    @IBOutlet weak var fatLabel: UITextField!
    @IBOutlet weak var datePicker: UIButton!
    @IBOutlet weak var trainingView: UITableView!

    //UserDefaultsのインスタンス
    let userDefaults = UserDefaults.standard
    //日付表示変更用
    var changeDate : Date = Date()
    //テーブル表示用
    var trainList = [String]()
    //日付の取得
    let today: Date = Date()
    let dateFormatter = DateFormatter()

    override func viewDidLoad() {
        super.viewDidLoad()
        //datePickerからの日付を判定し、帰ってきた値をセットする
        dateFormatter.dateFormat = "yyyy/MM/dd"
        if changeDate == today {
            datePicker.setTitle(dateFormatter.string(from: today), for: .normal)
        } else {
            datePicker.setTitle(dateFormatter.string(from: changeDate), for: .normal)
        }
        //トレーニングリストの保存&呼び出し
        if userDefaults.array(forKey: "training") != nil {
            trainList = userDefaults.array(forKey: "training") as! [String]
            userDefaults.set(trainList, forKey: "training")
            userDefaults.synchronize()
        }
        //Delegate&DataSourceの呼び出し
        trainingView.delegate = self
        trainingView.dataSource = self
        trainingView.allowsMultipleSelection = true
        weightLabel.delegate = self
        fatLabel.delegate = self
    }
    //キーボード以外の箇所のタッチでキーボードを閉じる処理
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
    //キーボードを閉じる処理
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    //tableDelegate
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath)
        cell?.accessoryType = .checkmark
    }
    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at:indexPath)
        cell?.accessoryType = .none
    }
    //TableDataSource
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return trainList.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = trainList[indexPath.row]
        cell.selectionStyle = .none
        let selectedIndexPaths = tableView.indexPathsForSelectedRows
        if selectedIndexPaths != nil && (selectedIndexPaths?.contains(indexPath))! {
            cell.accessoryType = .checkmark
        } else {
            cell.accessoryType = .none
        }
        return cell
    }

    @IBAction func saveMemory(_ sender: UIButton) {
        performSegue(withIdentifier: "saveMemory", sender: nil)
    }
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        //入力値に空があればエラーを出す
        if segue.identifier == "saveMemory" {
            let nextView = segue.destination as! ViewController
            //            空の場合は何もせず結果の画面へ遷移
            if weightLabel.text! != "" || fatLabel.text! != "" {
                let xs = self.trainingView.indexPathsForSelectedRows
                if xs != nil {
                    for x in xs! {
                        nextView.trainList.append(trainList[x.row])
                    }
                }
                nextView.weightText = Double(weightLabel.text!)!
                nextView.fatText = Double(fatLabel.text!)!
                nextView.dateText = datePicker.titleLabel!.text!
            }
        }
    }
}

下記がチャート画面です。

charts
import UIKit
import Charts

class ViewController: UIViewController,ChartViewDelegate,UITableViewDelegate,UITableViewDataSource {

    @IBOutlet weak var chartsView: LineChartView!
    @IBOutlet weak var dateLabel: UILabel!
    @IBOutlet weak var weightLabel: UILabel!
    @IBOutlet weak var fatLabel: UILabel!
    @IBOutlet var trainedList: UITableView!
    let userDefaults = UserDefaults.standard
    let weightString = "Weight"
    let fatString = "Fat"

    var weightDic = [String:Double]()
    var fatDic = [String:Double]()
    var trainDic = [String:[String]]()
    var weightList = [Double]()
    var fatList = [Double]()
    var weightText = 0.0
    var fatText = 0.0
    var dateText = ""
    var chartDataCount = 0
    var daysList = [String]()
    var trainList = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()
        chartsView.delegate = self

        let weightKey = weightString + dateText
        let fatKey = fatString + dateText

        loadData()
        if weightKey != "Weight" || fatKey != "Fat" {
            saveData(weightKey : weightKey, fatKey: fatKey, daysText:dateText)
        } else if dateText == "" {
            let weightSort = weightDic.keys.sorted()
            let fatSort = fatDic.keys.sorted()

            for key in weightSort {
                weightList.append(weightDic[key]!)
                daysList.append(String(key.suffix(5)))
            }
            for key in fatSort {
                fatList.append(fatDic[key]!)
            }
        }
        setChart(days:daysList)
        trainList = [String]()
    }
    @IBAction func clearUserDefault(_ sender: Any) {
        print("Clear")
        let alert: UIAlertController = UIAlertController(title: "データ消去", message: "これまでのデータを消去しますか?", preferredStyle: UIAlertController.Style.actionSheet)
        let comit = UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler:{
                (action: UIAlertAction!) -> Void in
            self.userDefaults.removeObject(forKey: "weight")
            self.userDefaults.removeObject(forKey: "fat")
            self.daysList = [""]
            self.weightList = [Double]()
            self.fatList = [Double]()
            self.dateLabel.text! = ""
            self.weightLabel.text! = ""
            self.fatLabel.text! = ""
            self.trainList = [String]()
            self.setChart(days: self.daysList)
            self.trainedList.reloadData()
            super.viewDidLoad()
        })
        let cancel = UIAlertAction(title: "戻る", style: UIAlertAction.Style.cancel, handler: nil)
        alert.addAction(comit)
        alert.addAction(cancel)
        present(alert, animated: true, completion: nil)
    }
    //userDefaultsからデータをDictionaryに代入
    //データがなければスルー(1回目とクリア後)
    func loadData() {
        if userDefaults.dictionary(forKey: "weight") != nil {
            weightDic = userDefaults.dictionary(forKey: "weight") as! [String : Double]
        }
        if userDefaults.dictionary(forKey: "fat") != nil {
            fatDic = userDefaults.dictionary(forKey: "fat") as! [String : Double]
        }
        if userDefaults.dictionary(forKey: "train") != nil {
            trainDic = userDefaults.dictionary(forKey: "train") as! [String:[String]]
        }
    }
    //渡ってきた値をuserDefaultsへセットし、リストへDictionaryValueset
    func saveData(weightKey : String,fatKey : String, daysText: String) {
        weightDic[weightKey] = weightText
        fatDic[fatKey] = fatText
        trainDic[daysText] = trainList

        userDefaults.set(weightDic, forKey: "weight")
        userDefaults.set(fatDic, forKey: "fat")
        userDefaults.set(trainDic, forKey: "train")
        userDefaults.synchronize()

        let weightSort = weightDic.keys.sorted()
        let fatSort = fatDic.keys.sorted()

        for key in weightSort {
            weightList.append(weightDic[key]!)
            daysList.append(String(key.suffix(5)))
        }
        for key in fatSort {
            fatList.append(fatDic[key]!)
        }
        setChart(days:daysList)
        trainList = [String]()
    }
    func setChart(days:[String]){
        let lineDefault = UIColor(red: 140.0/255.0, green: 234.0/255.0, blue: 255.0/255.0, alpha: 1.0)

        let data = LineChartData()
        var lineChartEntry1 = [ChartDataEntry]()

        for i in 0..<weightList.count {
            lineChartEntry1.append(ChartDataEntry(x: Double(i), y: Double(weightList[i])))
        }
        let line1 = LineChartDataSet(entries: lineChartEntry1, label: "体重")
        line1.drawCirclesEnabled = false
        line1.drawValuesEnabled = true
        line1.valueTextColor = UIColor.white
        line1.lineWidth = 2
        line1.setColor(UIColor.red)
        data.addDataSet(line1)

        if (fatList.count > 0) {
            var lineChartEntry2 = [ChartDataEntry]()
            for i in 0..<fatList.count {
                lineChartEntry2.append(ChartDataEntry(x: Double(i), y: Double(fatList[i])))
            }
            let line2 = LineChartDataSet(entries: lineChartEntry2, label: "体脂肪")
            line2.drawCirclesEnabled = false
            line2.drawValuesEnabled = true
            line2.valueTextColor = UIColor.white
            line2.lineWidth = 2
            line2.setColor(UIColor.blue)
            data.addDataSet(line2)
        }
        let chartFormatter = LineChartFormatter(labels: days)
        let xAxis = chartsView.xAxis
        xAxis.valueFormatter = chartFormatter
        xAxis.labelFont = UIFont(name: "HelveticaNeue-Light", size: 12.0)!
        xAxis.labelTextColor = UIColor.white
        chartsView.xAxis.granularityEnabled = true
        chartsView.xAxis.granularity = 1.0
        chartsView.xAxis.decimals = 0
        chartsView.xAxis.valueFormatter = xAxis.valueFormatter
        chartsView.data = data
    }

    private class LineChartFormatter: NSObject, IAxisValueFormatter {

        var labels: [String] = []

        func stringForValue(_ value: Double, axis: AxisBase?) -> String {
            if labels.count == 1 {
                return labels[0]
            } else {
                return labels[Int(value)]
            }
        }
        init(labels: [String]) {
            super.init()
            self.labels = labels
        }
    }
    func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
        let weightSort = weightDic.keys.sorted()
        let fatSort = fatDic.keys.sorted()
        var weightKey :String
        var fatKey : String
        var dateString : String

        if highlight.dataSetIndex == 0 {
            weightKey  = weightSort[NSInteger(entry.x)]
            fatKey = "Fat" + weightSort[NSInteger(entry.x)].suffix(10)
            dateString = String(weightSort[NSInteger(entry.x)].suffix(10))
        } else {
            weightKey  = "Weight" + fatSort[NSInteger(entry.x)].suffix(10)
            fatKey = fatSort[NSInteger(entry.x)]
            dateString = String(fatSort[NSInteger(entry.x)].suffix(10))
        }
        let weightString = weightDic[weightKey]
        let fatString = fatDic[fatKey]
        trainDic = userDefaults.dictionary(forKey: "train") as! [String : [String]]
        print(trainDic)
        if trainDic[dateString] != nil {
            trainList = trainDic[dateString]!
        } else {
            trainList = [String]()
        }

        dateLabel.text = dateString
        dateLabel.textColor = UIColor.white
        weightLabel.text = String(weightString!) + "kg"
        weightLabel.textColor = UIColor.white
        fatLabel.text = String(fatString!) + "%"
        fatLabel.textColor = UIColor.white
        trainedList.reloadData()
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return trainList.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = trainList[indexPath.row]
        return cell
    }
}

もっと綺麗なやり方があるんだろうなあ。。。

一応画面です。

IMG_3066.PNG
IMG_3067.PNG

恥ずかしながらまだSEなんですがw

参考になればいいし、もっとこうした方がってのがあれば教えてください!

profile

生まれも育ちも大阪の浪速中の浪速っ子が30才未経験からITエンジニアとして生きるブログもやってます。
よかったらみてください:muscle_tone2:

PVアクセスランキング にほんブログ村

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

【Swift】定数と変数の宣言に関する基本的な挙動

この投稿は何?

Swiftにおける変数と定数の宣言・定義の挙動について、調べたことを忘備録として残しておきました。

環境

macOS10.15 Catalina beta9
Xcode11

ハンズオン

まず最初に、宣言と定義について自分なりには次のように解釈しています。

  • 宣言: 値を割り当てるオブジェクトの名前とそのデータ型を決定すること。
  • 定義: 宣言に加えて、具体的な値またはnilを割り当てること。

上記を踏まえて、Playground環境で実行したコードは下記の通りです。

Variables 変数の挙動

変数として宣言されたオブジェクトには、何度でも新しい値を割り当てることができます。
ただし、具体的な値を割り当てる前にアクセスすると、エラーになります。
これは、未初期化状態であることが原因です。

変数オブジェクトを宣言して、値を更新する
var number: Int // declared Integer type variable.
number          // ERROR: variable 'number' used before being initialized number
number = 123    // number is assigned value.
number = 321    // number is assigned value agein.

変数オブジェクトをオプショナルとして宣言します。
すると、宣言された時点で nil自動的に既定値として割り当てられることが確認できます。

オプショナルの場合
var optionalNumber: Int? // declared Optional Integer type variable.
optionalNumber           // nil is assigned as default value.
optionalNumber = 456     // assigned new value.

Constants 定数の挙動

定数オブジェクトの挙動です。
オブジェクトを宣言した後は、1度だけ値を割り当てることができます。
やはり、既定値を割り当てる前にアクセスすると、エラーになります。

定数オブジェクトを宣言して、値を割り当てる
let message: String     // declared String type constant.
message                 // ERROR: constant 'message' used before being initialized message
message = "Hello"       // Constant, but can be assigned a value only once.

以下のコードでは、宣言と同時に既定値を割り当てて、定数オブジェクトalphabetを定義しています。
既定値として値を一度、割り当てたので、この定数alphabetの値は変更できません。

定数オブジェクトを定義する
let alphabet = "abc"    // define a String type constant.
alphabet = "defg"    // ERROR: Cannot assign to value

今度は、オプショナルの場合です。
オプショナルの定数オブジェクトoptionalAlphabetを宣言します。
変数のオプショナルオブジェクトでは、既定値としてnilが自動的に割り当てられていました。
ところが、定数のオプショナルでは既定値が割り当てられていません。
optionalAlphabetにアクセスすると、未初期化状態のエラーが警告されます。

オプショナル定数の場合
let optionalAlphabet: String?   // declared Optional String type Constant.
optionalAlphabet                // ERROR: constant 'optionalAlphabet' used before being initialized
optionalAlphabet = "ABC"        // assigned default value.

定数オブジェクトに対して自動的にnilを割り当ててしまうと、1度目の値を割り当てたことになり任意の値を割り当てられなくなってしまいます。
そのため、このような挙動になっていると考えられます。

考察

安全を謳うSwiftプログラミングでは「まずは定数で」というコンセプトがあります。
その後、値を変更する必要が発生したら、変数オブジェクトに修正するのが常套手段です。
変数オブジェクトはオプショナルとして宣言すると、自動的にnilが既定値になります。
しかし、定数オブジェクトがオプショナルな場合、意図的に既定値を割り当てないとエラーになります。
呼び出すだけでエラーになるので、nilと比較することすらできません。
ビルド前の静的エラーとして検出されるので、それほど危険なわけではありませんが覚えておきたい挙動です。

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

ARKitのdaeを使って表示しているオブジェクトがなぜ荒いのかを解決した話

はじめに

ARKitを使ったSNSアプリARIZARを開発しています。
開発している時にわかったTIPS的なネタを記事にします。

ぜひARIZARをダウンロードして使ってみてください。
よろしくお願いします。

今回の課題

2つのツイートをみていただくとわかりますが、1つ目より2つ目の方がアバターと私達が呼んでいるオブジェクトがキレイになっていると思います。
今回は、解決したことについて記事にします。

なぜ荒いの?

前回の記事にその答えが隠されていました。

要するに、大きな文字を描画して小さく縮小すれば良いのではないかということがわかりました。

これです。実際のscnファイルを確認しました。

before.png

Positonが(0,-100,-200)にScaleが(0.361,0.361,0.361)になっています。
これは、PositionのZ軸が-200となっていてこれは自分から200m離れていることになります。

でもiPhoneの画面上、オブジェクトが荒い以外違和感がないのですが、逆に言うとオブジェクトが巨大だということが冷静になってみるとわかります。
もう1つ違和感があったのがiPhoneを左右に振って視点を少し変えてもオブジェクトがあまり動かないことの不思議に思っていました。

よくよく考えてみると、東京から富士山をみているような感じなのかなっと気づきました。

解決策

PositionとScaleを調整して、Positionは近く、Scaleはもっと小さくすることでオブジェクトを目の前に移動させました。
これにより本来のオブジェクトの姿が見えるようになりました。
こちらはコードで40cm手前に配置するようにしました。

改善後
        let model = SCNScene(named: named)
        if let modelNode = model?.rootNode.childNodes.first {
            modelNode.position = SCNVector3Make(0, -0.03, -0.4)
            modelNode.scale = SCNVector3(0.0006,0.0006,0.0006)
        }

最後に

ARは物体がないので座標系がピンとこないことが多いです。
荒かったり、カクカクする場合にはそのオブエクトが遠すぎないか?の観点で点検してみてはいかがでしょうか?
ありがとうございました。

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

ARKitのdaeを使って表示しているオブジェクトがなぜか荒いのを解決した話

はじめに

ARKitを使ったSNSアプリARIZARを開発しています。
開発している時にわかったTIPS的なネタを記事にします。

ぜひARIZARをダウンロードして使ってみてください。
よろしくお願いします。

今回の課題

2つのツイートをみていただくとわかりますが、1つ目より2つ目の方がアバターと私達が呼んでいるオブジェクトがキレイになっていると思います。
今回は、解決したことについて記事にします。

なぜ荒いの?

前回の記事にその答えが隠されていました。

要するに、大きな文字を描画して小さく縮小すれば良いのではないかということがわかりました。

これです。実際のscnファイルを確認しました。

before.png

Positonが(0,-100,-200)にScaleが(0.361,0.361,0.361)になっています。
これは、PositionのZ軸が-200となっていてこれは自分から200m離れていることになります。

でもiPhoneの画面上、オブジェクトが荒い以外違和感がないのですが、逆に言うとオブジェクトが巨大だということが冷静になってみるとわかります。
もう1つ違和感があったのがiPhoneを左右に振って視点を少し変えてもオブジェクトがあまり動かないことの不思議に思っていました。

よくよく考えてみると、東京から富士山をみているような感じなのかなっと気づきました。

解決策

PositionとScaleを調整して、Positionは近く、Scaleはもっと小さくすることでオブジェクトを目の前に移動させました。
これにより本来のオブジェクトの姿が見えるようになりました。
こちらはコードで40cm手前に配置するようにしました。

改善後
        let model = SCNScene(named: named)
        if let modelNode = model?.rootNode.childNodes.first {
            modelNode.position = SCNVector3Make(0, -0.03, -0.4)
            modelNode.scale = SCNVector3(0.0006,0.0006,0.0006)
        }

最後に

ARは物体がないので座標系がピンとこないことが多いです。
荒かったり、カクカクする場合にはそのオブエクトが遠すぎないか?の観点で点検してみてはいかがでしょうか?
ありがとうございました。

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

Human Interface Guidelines App Architecture まとめ

1. Loading ローディング

アプリが何か処理をしている際に画面が動かなかったり、真っ白になるとアプリがフリーズしたように見えてしまい、ユーザーがイライラしてアプリを終了したりアンインストールする可能性が上がるためロードしていることをユーザーに示す必要がある。

ローディング画面を作る

方法1

Loading Spinner(以下の写真のようなクルクルしたやつ)を表示する。
images.jpeg

Loading Spinnerを簡単に表示する方法

方法2

progress barを使い、進捗をユーザーに示す。
e303fe47.png

方法3

処理をしている間にチュートリアルなどを見せる。
clashofclanstutorial01.jpg

2. Modality (分岐型)

モーダリティーはユーザの注目を集めるデザインの手法です。特定の処理をするときに、別画面やViewを前の画面に重ねて表示する手法です。例えばAir Dropするときに下から出てくる画面のようなものを指します。
ios12-iphone-x-photos-share-airdrop.jpg

手法1:アラートビュー

iOS-Simulator-Screen-shot-30-Dec-2013-09.50.43.png
このように画面の上にビューを重ねることでユーザーの注目を引くことができます。

手法2:モーダルビュー

Modality_2x.png

手法3:シートビュー

ActionSheet_Iphone.png

これらを使うときの注意点

これらの手法をむやみやたらに使ってもユーザーが使いごごちのいいアプリを作ることはできません。
以下のことを心がけて使用する必要があります。

モーダルビューは本当に必要な時にしか使わない。

モーダルビューはユーザが今までしていたタスクとは違うことをしてもらう時に使います。なのでユーザーが本当にその作業をすべきなのか考え、必要なときに使いましょう。

アラートビューは本当に必要な時のみ使う。

アラートビューは使いやすく、むやみやたらに使われやすい傾向がありますが、むやみやたらに表示するとユーザーの操作を邪魔してしまうので本当に必要な時にのみ使いましょう。

モーダルビューの内容、することは少なめに

モーダルビューでたくさんのことをユーザーにしてもらうと、ユーザーが何をするためにモーダルビューを表示させたのかわかりにくくなってしまう傾向があります。なので、モーダルビューのコンテンツは極力少なくしましょう。そして、完了(Done)ボタンは本当にユーザーがタスクを終わらせた時のみに使いましょう。

モーダルビューにキャンセルボタンを必ずつけましょう。

モーダルビューを表示する際にそれをまたしまえるようにキャンセルボタンを設置しましょう。

ユーザーが入力したデータが消えるのを防ぎましょう。

ユーザーが何かをモーダルビューで入力した際に間違えてキャンセルジェスチャーやキャンセルボタンを押してしまう可能性があります。モーダルビューがキャンセルされる前にはデータが消えてしまうことをユーザーに必ず示しましょう。

ポップオーバーの上には何も表示しない。

ポップオーバーの上にはカードなどを表示することができますが、表示する前にはポップオーバーをしまってから表示するようにしましょう。
ポップオーバー
MWGUp.png

モーダルビューにナビゲーションバーなどをつける場合は元のビューと同じデザインに

モーダルビューの表示のアニメーションは適切なものを使いましょう。

3ナビゲーション

階層ナビゲーション

一つの画面に選択する項目を一つしか置かない手法。初めの画面に戻るには手順を画面を一つずつ遡っていく必要がある。メールや設定などのアプリはこの手法を使っている。
NavigationHierarchical-Graphic_2x.png

フラットナビゲーション

全ての画面からタブバーなどを使い全ての画面にアクセスできるナビゲーション。ミュージックアプリやApp Storeなどで使われいる。
NavigationFlat-Graphic_2x.png

コンテンツ主従型

基本的には使われない型だが、ゲームなどではこのようなナビゲーション型を使う場合がある。
NavigationExperienceDriven-Graphic_2x.png

引用元

Apple Human Interface Guidelines (かなり意訳してまとめました。)

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

【Swift】構造体とJSONの変換方法(JSONEcoder,decoder)

API通信する際に構造体をJSONに変換してリクエストしたりまた逆に、APIから受け取ったJSONのレスポンスを構造体に変換したい時があります。

そこで、今回はJSONDecoer,JSONEncoderを用いて構造体とJSONの変換方法を備忘録として記載します。(Qiita初投稿なので、緊張してます)

バーション

Xcode10.2.1、Swift5.0.1

JSONEncoder(構造体→JSON)

// Human構造体
struct Human:Codable {
    var id: Int
    var name: String
    var hobby: String
}

// Humanのインスタンス生成
let hogeMan1 = Human(id: 1, name: "ほげ君", hobby: "プログラミング")
print(hogeMan1)
// -> "Human(id: 1, name: "ほげ君", hobby: "プログラミング")"

// JSONEcoder(構造体→JSON[Data型])
let hogeManJsonData = try! JSONEncoder().encode(hogeMan1)
print(hogeManJsonData)
// -> "59 bytes"

// Data型→String型にキャスト
let hogeManJsonString = String(data: hogeManJsonData, encoding: .utf8)!
print(hogeManJsonString)
// -> "{"id":1,"name":"ほげ君","hobby":"プログラミング"}"

ポイント

  • EncodeとDecodeを行うため、構造体にCodebleを継承する
  • JSONEncoderの戻り値はData型なので、必要に応じてString型にキャストする

Codable = Encodable + Decodable
引用:【Swift】Codableについて備忘録

JSONDecoder(JSON→構造体)

// JSON[String型]定義
let jsonString = "{\"id\":2,\"name\":\"ほげほげ君\",\"hobby\":\"ボルダリング\"}"

// String型→Data型にキャスト
let jsonData = jsonString.data(using: .utf8)!
print(jsonData)
// -> "62 bytes"

// JSONDecoder(JSON→構造体)
let hogeMan2 = try! JSONDecoder().decode(Human.self, from: jsonData)
print(hogeMan2)
// -> "Human(id: 1, name: "ほげ君", hobby: "プログラミング")"

ポイント

  • JSONDecoderの引数はData型なので、String型のJSONはData型にキャストする必要がある

まとめ

上記で紹介したコードのまとめです。

import UIKit

// Human構造体
struct Human:Codable {
    var id: Int
    var name: String
    var hobby: String
}

// JSONEncoder--------------------------------------------------
// Humanのインスタンス生成
let hogeMan1 = Human(id: 1, name: "ほげ君", hobby: "プログラミング")
print(hogeMan1)

// JSONEcoder(構造体→JSON[Data型])
let hogeManJsonData = try! JSONEncoder().encode(hogeMan1)
print(hogeManJsonData)

// Data型→String型にキャスト
let hogeManJsonString = String(data: hogeManJsonData, encoding: .utf8)!
print(hogeManJsonString)

// JSONDecoder--------------------------------------------------
// JSON[String型]定義
let jsonString = "{\"id\":2,\"name\":\"ほげほげ君\",\"hobby\":\"ボルダリング\"}"

// String型→Data型にキャスト
let jsonData = jsonString.data(using: .utf8)!

// JSONDecoder(JSON→構造体)
let hogeMan2 = try! JSONDecoder().decode(Human.self, from: jsonData)
print(hogeMan2)

以上になります。
もし不明点や間違い等があればコメントくださいm(_ _)m

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

[SwiftUI]型消去を利用した任意のViewをPresentする手法

SwiftUIでは、モーダル表示をするためにsheetというメソッドを利用します。

struct ContentView: View {

    @State var isPresented: Bool = false

    var body: some View {
        VStack {
            Button("present") {
                self.isPresented.toggle()
            }
        }
        .sheet(isPresented: $isPresented) {
            Text("Presenting")
        }
    }
}

しかし、このsheetメソッドは後勝ちで実装を保持するため複数回呼ぶと最後のsheetだけが有効になります。

この挙動を回避するにはsheet(item:content:)を利用します。
これはBinding<Identifiable>を受け取り、それをもとにViewを返す方法です。

この挙動に関する解説は

@1amageek SwiftUIのsheetを出し分ける

をお読みください。

上記記事では、IdentifiableViewの性質をenumに持たせ
.sheet(item: self.$presentation) { $0 }
のように遷移させています。こうすることで、親Viewは複数の要素を管理する必要もなくなり、分岐のロジックをもつ必要もなくなります。

しかし、この手法はViewが遷移先がどこで管理されているかを知る必要があり、また遷移先はpresentationで宣言された型に限定されます。

IdentifiableViewの性質を持たせつつ、より柔軟に遷移先を制御する方法を考えてみました。

実装は、次のように型消去を用いて具体的な型を作ります。

struct AnyIdentifiableView: View, Identifiable {
    typealias ID = AnyHashable
    private let _id: ID
    var id: ID {
        return _id
    }

    private let _body: AnyView
    var body: some View {
        _body
    }

    init?<V>(view: V?) where V: View & Identifiable {
        guard let view = view else { return nil }
        self._body = AnyView(view)
        self._id = view.id
    }
}
extension Identifiable where Self: Hashable {
    public typealias ID = Self
    public var id: Self { self }
}

AnyIdentifiableViewは、IdentifiableViewの性質を持つ構造体です。
実態はinit?<V>(view: V?) where V: View & Identifiableで受け取ったViewになります。
IdentifiableViewはそれぞれAnyHashableAnyViewに分解されて保持されました。
IdentifiableはHashableに準拠した型に限り、protocol extensionによってidを自動的に実装します。
これで、View & Identifiableを自由に入れる事ができる型が出来ました。

そして、AnyIdentifiableViewは具体的な型なので、特に複雑なことをせずに@Stateで宣言できます。

struct ContentView: View {

    @State var presentation: AnyIdentifiableView? = nil

    var body: some View {
        VStack {
            Button("present") {
                self.isPresented.toggle()
            }
        }
        .sheet(item: self.$presentation) { $0 }
    }
}

ViewModelが複数ある場合でも、View, Hashable, Identifiableに準拠した型を渡せば柔軟に遷移する事が出来るようになりました。

viewModel1.$presentation.map({ PresentationView(view: $0) }).assign(to: \.presentation, on: self)
viewModel2.$presentation.map({ PresentationView(view: $0) }).assign(to: \.presentation, on: self)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む