- 投稿日:2019-09-29T22:22:43+09:00
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
orfill
を選択する。以上、他にまた類似の指定方法があって迷いそうだったら追加したいです。
間違い等あれば教えていただければ嬉しいです!
- 投稿日:2019-09-29T19:05:20+09:00
Swiftでルビを表示させたい件について
どうでもいいこと
先日iOSDCに参加して以降ためこんでいた発信欲をそろそろ解放したいと思い、例によって備忘録でしかない内容をiOSエンジニア初心者の自分のために書きます。(ぜひ来年はLT枠とかで登壇したい...!)
Swiftでルビを表示させたい初心者へ
今回は仕事で少し触れそうなので、ルビ表示をSwiftで実装する、というテーマです。
ほとんど自分のためとはいえ、私のような初心者のためになることもあるのではという前向きな気持ちも抱きながら書いています。
以下の2つの記事を全面的に参考にさせていただきましたが、こちらで簡潔にまとめられている説明を、わざわざくどく、面倒くさく、まどろっこしく初心者向けにしたものがこの記事です。
(ルビ表示の実装が初心者に需要ある?という自分へのツッコミをスルーしながら書いてます。)
- https://qiita.com/woxtu/items/284369fd2654edac2248
- https://qiita.com/negi0205/items/6c73128ff2cf680df47c
実装したコードがこちら
実装の主な方針は以下の通りです。
- 前提として
- 環境:Xcode10.3、Swift5
- 青空文庫の表記(「紅玉《ルビー》」)をお借りしてルビ付文字のStringを表現
- 正規表現を使ってString内のルビ付文字を特定 → StringExtension.swiftを参照
- CTRubyAnnotationとNSAttributedStringを使ってルビを生成 → StringExtension.swiftを参照
- カスタムUILabelクラスを作ってルビを表示 → RubyLabel.swiftとRubyViewController.swiftを参照
ではさっそく、正規表現を使ってルビ付文字を特定し、ルビを生成する実装です。
StringExtension.swiftimport 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.swiftimport 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のサイズを調整するなどします。ルビが表示されました!
参考までにViewController(UILabelは上記のカスタムクラスを継承)はこんな感じにしています。
RubyViewController.swiftimport 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) } }
- 投稿日:2019-09-29T17:28:08+09:00
iOS13のPageSheetでdismissを呼んだ時にもpresentationControllerDidDismissを呼ぶ話
はじめに
こんにちは、fummicc1です。今回は、iOS13から変更された
modalPresentationStyle
に.pageSheet
を設定した場合の挙動を説明した後に、実際にどういうコードを書くのがいいのかを紹介します。iOS13のページシート
iOS13からiPhoneでページシートの挙動が大きく変わりました。そして、デフォルトでページシートで遷移されるようになりました。
今まで(iOS13以前)は.fullScreen
と同じ挙動だったのですが、今回から被さるようなUIになりました。(画像参照)
まず、これによって以下のようなAPIが呼ばれなくなりました。
- viewWillAppear
- viewWillDisappear
これは既存のアプリケーションを破壊する可能性があるので至急対応しているところが多いと思います。
例えば、model遷移先の画面で何か日記やToDoを作成し、saveボタンを押して保存処理をし画面をdismissします。
その場合、従来はviewWillAppear
でcollectionView
やtableView
などのUIの更新処理をかけている場合多いです。しかし、pageSheetによる遷移ではdismissしても
viewWillAppear
は呼ばれないので、日記やToDoを保存した後の状態がUIに反映されません。今回は、これに対応する方法を紹介します。対応策
1.
.fullScreen
にする。
modalPresentationStyle
に.pageSheet
ではなく、.fullScreen
を指定します。そうすれば既存の手法で話を進められます。2.
.pageSheet
のままで実装する。presentationControllerDidDismissというメソッドがiOS13から追加されているので、それを使用します。
このメソッドは遷移先の画面のインスタンスで
presentationController?.delegate = self
を実装します。このとき、self(遷移元の画面)
はUIAdaptivePresentationControllerDelegate
に準拠している必要があります。また、この
UIAdaptivePresentationControllerDelegate
はpresentationControllerDidDismiss
を所持しているので以下のように実装します。RootViewController.swiftfunc transitionToModalViewController() { let modalViewController = storyboard?.instantiateViewController(identifier: "ModalViewController") as! ModalViewController modalViewController.presentationController?.delegate = self // ここがポイント present(modalViewController, animated: true, completion: nil) }RootViewController.swiftextension RootViewController: UIAdaptivePresentationControllerDelegate { // このメソッドを実装 func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // Modal画面から戻った際の画面の更新処理を行う。 (collectionView.reloadDataなど。) } }これで、ユーザーがModal画面で下からスワイプをしたときにも更新処理ができます。
dismissでModalを閉じると
presentationControllerDidDismiss
が呼ばれない。これの対処法は、ベストプラクティスではないような気もしますが、遷移作の画面で
dismiss
をオーバーライドし、その中でpresentationController.delegate?.presentationControllerDidDismiss?(presentationController)
を呼びます。ModalViewController.swiftextension 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を改善するなり、もっといい方法があるといいなと思いっています。
修正・意見があればコメントお待ちしています。参考になれば幸いです!
- 投稿日:2019-09-29T15:13:59+09:00
【swift】chartsを使って体重管理アプリを作ってみた
入力値がグラフに反映される実装をしてみました
流れとしてはこんな感じ
初期画面で体重と体脂肪率を入力⇨登録ボタンで画面遷移、その日の情報が登録されてグラフに表示、グラフタップでその日の情報が表示されるって感じです。
コードメインになります。
メイン画面の方はこんな感じです。
mainimport 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! } } } }下記がチャート画面です。
chartsimport 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へセットし、リストへDictionaryのValueをset 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 } }もっと綺麗なやり方があるんだろうなあ。。。
一応画面です。
恥ずかしながらまだSEなんですがw
参考になればいいし、もっとこうした方がってのがあれば教えてください!
profile
生まれも育ちも大阪の浪速中の浪速っ子が30才未経験からITエンジニアとして生きるブログもやってます。
よかったらみてください
- 投稿日:2019-09-29T15:00:17+09:00
【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
と比較することすらできません。
ビルド前の静的エラーとして検出されるので、それほど危険なわけではありませんが覚えておきたい挙動です。
- 投稿日:2019-09-29T14:18:14+09:00
ARKitのdaeを使って表示しているオブジェクトがなぜ荒いのかを解決した話
はじめに
ARKitを使ったSNSアプリARIZARを開発しています。
開発している時にわかったTIPS的なネタを記事にします。ぜひARIZARをダウンロードして使ってみてください。
よろしくお願いします。今回の課題
2つのツイートをみていただくとわかりますが、1つ目より2つ目の方がアバターと私達が呼んでいるオブジェクトがキレイになっていると思います。
今回は、解決したことについて記事にします。ハイタッチで終わり、また来年?
— 藤 治仁 (@From_F) September 7, 2019
少し恥ずかしかった?#iosdc #arizar #ar pic.twitter.com/31rvqO3wcU開発中の #ARIZAR 謎だったところが、だんだん意味がわかってきた。#ARKit が少しづつ使いこなせるようになってきた。まだまだだけど…#ar #swift pic.twitter.com/wUPOoSUG5B
— 藤 治仁 (@From_F) September 16, 2019なぜ荒いの?
前回の記事にその答えが隠されていました。
要するに、大きな文字を描画して小さく縮小すれば良いのではないかということがわかりました。
これです。実際のscnファイルを確認しました。
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は物体がないので座標系がピンとこないことが多いです。
荒かったり、カクカクする場合にはそのオブエクトが遠すぎないか?の観点で点検してみてはいかがでしょうか?
ありがとうございました。
- 投稿日:2019-09-29T14:18:14+09:00
ARKitのdaeを使って表示しているオブジェクトがなぜか荒いのを解決した話
はじめに
ARKitを使ったSNSアプリARIZARを開発しています。
開発している時にわかったTIPS的なネタを記事にします。ぜひARIZARをダウンロードして使ってみてください。
よろしくお願いします。今回の課題
2つのツイートをみていただくとわかりますが、1つ目より2つ目の方がアバターと私達が呼んでいるオブジェクトがキレイになっていると思います。
今回は、解決したことについて記事にします。ハイタッチで終わり、また来年?
— 藤 治仁 (@From_F) September 7, 2019
少し恥ずかしかった?#iosdc #arizar #ar pic.twitter.com/31rvqO3wcU開発中の #ARIZAR 謎だったところが、だんだん意味がわかってきた。#ARKit が少しづつ使いこなせるようになってきた。まだまだだけど…#ar #swift pic.twitter.com/wUPOoSUG5B
— 藤 治仁 (@From_F) September 16, 2019なぜ荒いの?
前回の記事にその答えが隠されていました。
要するに、大きな文字を描画して小さく縮小すれば良いのではないかということがわかりました。
これです。実際のscnファイルを確認しました。
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は物体がないので座標系がピンとこないことが多いです。
荒かったり、カクカクする場合にはそのオブエクトが遠すぎないか?の観点で点検してみてはいかがでしょうか?
ありがとうございました。
- 投稿日:2019-09-29T12:22:32+09:00
Human Interface Guidelines App Architecture まとめ
1. Loading ローディング
アプリが何か処理をしている際に画面が動かなかったり、真っ白になるとアプリがフリーズしたように見えてしまい、ユーザーがイライラしてアプリを終了したりアンインストールする可能性が上がるためロードしていることをユーザーに示す必要がある。
ローディング画面を作る
方法1
Loading Spinner(以下の写真のようなクルクルしたやつ)を表示する。
方法2
方法3
2. Modality (分岐型)
モーダリティーはユーザの注目を集めるデザインの手法です。特定の処理をするときに、別画面やViewを前の画面に重ねて表示する手法です。例えばAir Dropするときに下から出てくる画面のようなものを指します。
手法1:アラートビュー
このように画面の上にビューを重ねることでユーザーの注目を引くことができます。手法2:モーダルビュー
手法3:シートビュー
これらを使うときの注意点
これらの手法をむやみやたらに使ってもユーザーが使いごごちのいいアプリを作ることはできません。
以下のことを心がけて使用する必要があります。モーダルビューは本当に必要な時にしか使わない。
モーダルビューはユーザが今までしていたタスクとは違うことをしてもらう時に使います。なのでユーザーが本当にその作業をすべきなのか考え、必要なときに使いましょう。
アラートビューは本当に必要な時のみ使う。
アラートビューは使いやすく、むやみやたらに使われやすい傾向がありますが、むやみやたらに表示するとユーザーの操作を邪魔してしまうので本当に必要な時にのみ使いましょう。
モーダルビューの内容、することは少なめに
モーダルビューでたくさんのことをユーザーにしてもらうと、ユーザーが何をするためにモーダルビューを表示させたのかわかりにくくなってしまう傾向があります。なので、モーダルビューのコンテンツは極力少なくしましょう。そして、完了(Done)ボタンは本当にユーザーがタスクを終わらせた時のみに使いましょう。
モーダルビューにキャンセルボタンを必ずつけましょう。
モーダルビューを表示する際にそれをまたしまえるようにキャンセルボタンを設置しましょう。
ユーザーが入力したデータが消えるのを防ぎましょう。
ユーザーが何かをモーダルビューで入力した際に間違えてキャンセルジェスチャーやキャンセルボタンを押してしまう可能性があります。モーダルビューがキャンセルされる前にはデータが消えてしまうことをユーザーに必ず示しましょう。
ポップオーバーの上には何も表示しない。
ポップオーバーの上にはカードなどを表示することができますが、表示する前にはポップオーバーをしまってから表示するようにしましょう。
ポップオーバー
モーダルビューにナビゲーションバーなどをつける場合は元のビューと同じデザインに
モーダルビューの表示のアニメーションは適切なものを使いましょう。
3ナビゲーション
階層ナビゲーション
一つの画面に選択する項目を一つしか置かない手法。初めの画面に戻るには手順を画面を一つずつ遡っていく必要がある。メールや設定などのアプリはこの手法を使っている。
フラットナビゲーション
全ての画面からタブバーなどを使い全ての画面にアクセスできるナビゲーション。ミュージックアプリやApp Storeなどで使われいる。
コンテンツ主従型
基本的には使われない型だが、ゲームなどではこのようなナビゲーション型を使う場合がある。
引用元
Apple Human Interface Guidelines (かなり意訳してまとめました。)
- 投稿日:2019-09-29T11:32:00+09:00
【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
- 投稿日:2019-09-29T05:53:02+09:00
[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を返す方法です。この挙動に関する解説は
をお読みください。
上記記事では、
Identifiable
とView
の性質をenumに持たせ
.sheet(item: self.$presentation) { $0 }
のように遷移させています。こうすることで、親Viewは複数の要素を管理する必要もなくなり、分岐のロジックをもつ必要もなくなります。しかし、この手法はViewが遷移先がどこで管理されているかを知る必要があり、また遷移先は
presentation
で宣言された型に限定されます。
Identifiable
とView
の性質を持たせつつ、より柔軟に遷移先を制御する方法を考えてみました。実装は、次のように型消去を用いて具体的な型を作ります。
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
は、Identifiable
とView
の性質を持つ構造体です。
実態はinit?<V>(view: V?) where V: View & Identifiable
で受け取ったViewになります。
Identifiable
とView
はそれぞれAnyHashable
とAnyView
に分解されて保持されました。
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)