- 投稿日:2020-09-16T22:07:44+09:00
【Swift】iOSで取得できる位置情報を実測してみた
iOSでの取得できる位置情報がどの程度正確かが気になったので、実測してみた。
前提知識
iOSにおいて、位置情報の精度は以下の6パターンを設定することが可能。
No. 値 補足 1 kCLLocationAccuracyBestForNavigation ナビアプリなどで利用できる最高精度 2 kCLLocationAccuracyBest 最高精度(デフォルト値) 3 kCLLocationAccuracyNearestTenMeters 誤差10m程度の精度 4 kCLLocationAccuracyHundredMeters 誤差100m程度の精度 5 kCLLocationAccuracyKilometer 誤差1,000m程度の精度 6 kCLLocationAccuracyThreeKilometers 誤差3,000m程度の精度 測定方法
下図のルート(A地点からB地点)を歩いてみて、OSが取得する位置情報の経度/緯度をプロットしてみる。
OSが取得する位置情報とは、locationManager(_:didUpdateLocations:) で取得できる位置情報のこと。実測結果
- 投稿日:2020-09-16T21:19:51+09:00
ニュースアプリにコロナ関連の記事があったら、リジェクトされた。
1.リジェクトされた理由
ニュースアプリのAppStoreに申請をしたのですが、
コロナ関連の記事があったので、リジェクトされました。2.対策
コロナ関連の記事は削除しました。
3.注意点
今回は外部データをいじるだけだったので、新たにビルドする必要なかったら、
再申請では無く、問題解決センターに返信をしました。
こっちの方が早いからね!
再申請だとまた後ろから、並び直しだしね!4.宣伝
これがコロナ関連の記事が無くなってしまったニュースアプリです。
NewsTweet(ニューズツイート)
https://apps.apple.com/jp/app/newstweet-ニューズツイート/id1531315934
- 投稿日:2020-09-16T20:16:43+09:00
iOS14GMでデフォルトブラウザを変更した時のcanOpenURLがおかしいから整理してみた
何が起こっている?
iOS14 PublicBeta8以降を入れたiOS端末で、デフォルトブラウザを変更した際にブラウザ遷移が行えない。
発生条件
- iOS14PB版、もしくはGM版、正式版(iOS14.0)
- デフォルトブラウザをsafari以外に設定していること
デフォルトブラウザ設定方法
- 「設定」アプリ起動
- デフォルトブラウザにしたいアプリへ移動(例:Chrome,Microsoft Edge)
- 「デフォルトブラウザApp」を開く
- 標準ブラウザをSafariからChromeに変更する
原因
UIApplicationの
canOpenURL
メソッドがfalseを返却している。
これが、iOS14の仕様なのかバグなのかは現時点(日本時間2020/09/16)では不明。
canOpenURL returns false when I changed the default browser on iOS 14そもそもcanOpenURLとは?
Determine whether there is an installed app registered to handle a URL scheme (canOpenURL(_:))
(URLスキームを処理するために登録されたインストール済みアプリがあるかどうかを判別する)アプリから別のアプリを起動する(URLスキーム)際に、処理するためのアプリが端末に存在しているかを返す関数。
(そもそもここの認識が間違えていて、「全てのURL起動にcanOpenURLを通す」という誤用?が今回の事件を引き起こしたと思われる。私もその一人です)対応方法
その1:canOpenURLを通さない
そもそもiOS端末にはブラウザアプリとしてSafariが必ず入っている。
かつ長押ししてもsafariアプリは削除もできないので、
ブラウザ起動をしようとして、iOS端末にブラウザアプリがないことは有り得ない はず。
(とはいえ、ブラウザ起動でcanOpenURLがfalseを返すことはないはずが、今回起こっているわけですが)上記の通り、canOpenURLの目的は別のアプリの起動ができるかを確認するためのもののため、
ブラウザ起動のためのURL起動の場合は、canOpenURLを通す必要はなさそう。よっていきなり
open
を叩くように修正する。// AS-IS例 if UIApplication.shared.canOpenURL(URL(string: "https://qiita.com/")!) { UIApplication.shared.open(URL(string: "https://qiita.com/")!) } else { print("ブラウザ起動失敗") }// To-BE例 UIApplication.shared.open(URL(string: "https://qiita.com/")!)その2:plistにhttp/httpsを追加する
canOpenURLの判定には、plistの設定が反映されるためブラウザ起動のURLを許可する。
<key>LSApplicationQueriesSchemes</key> <array> <string>http</string> <string>https</string> </array>参考
公式:
UIApplication | Apple Developer Documentation(引用はこちらより)
canOpenURL(_:) | Apple Developer Documentation参考:
特定のアプリがインストール済みかチェックする
[iOS] ディープリンク(Custom URL Scheme)でアプリを起動する
免責:全てのiOS14のアプリの動作を保証する解決方法ではございません
間違っている点や他の方法があれば教えてください!
- 投稿日:2020-09-16T20:16:43+09:00
【iOS14】デフォルトブラウザを変更した時にcanOpenURLがfalseになる問題
何が起こっている?
iOS14 PublicBeta8以降を入れたiOS端末で、デフォルトブラウザを変更した際にブラウザ遷移が行えない。
発生条件
- iOS14PB版、もしくはGM版、正式版(iOS14.0)
- デフォルトブラウザをsafari以外に設定していること
デフォルトブラウザ設定方法
- 「設定」アプリ起動
- デフォルトブラウザにしたいアプリへ移動(例:Chrome,Microsoft Edge)
- 「デフォルトブラウザApp」を開く
- 標準ブラウザをSafariからChromeに変更する
原因
UIApplicationの
canOpenURL
メソッドがfalseを返却している。
これが、iOS14の仕様なのかバグなのかは現時点(日本時間2020/09/16)では不明。
canOpenURL returns false when I changed the default browser on iOS 14そもそもcanOpenURLとは?
Determine whether there is an installed app registered to handle a URL scheme (canOpenURL(_:))
(URLスキームを処理するために登録されたインストール済みアプリがあるかどうかを判別する)アプリから別のアプリを起動する(URLスキーム)際に、処理するためのアプリが端末に存在しているかを返す関数。
(そもそもここの認識が間違えていて、「全てのURL起動にcanOpenURLを通す」という誤用?が今回の事件を引き起こしたと思われる。私もその一人です)対応方法
その1:canOpenURLを通さない
そもそもiOS端末にはブラウザアプリとしてSafariが必ず入っている。
かつ長押ししてもsafariアプリは削除もできないので、
ブラウザ起動をしようとして、iOS端末にブラウザアプリがないことは有り得ない はず。
(とはいえ、ブラウザ起動でcanOpenURLがfalseを返すことはないはずが、今回起こっているわけですが)上記の通り、canOpenURLの目的は別のアプリの起動ができるかを確認するためのもののため、
ブラウザ起動のためのURL起動の場合は、canOpenURLを通す必要はなさそう。よっていきなり
open
を叩くように修正する。
これでデフォルトブラウザを変更していても、ちゃんとsafariではないブラウザアプリで起動される。// AS-IS例 if UIApplication.shared.canOpenURL(URL(string: "https://qiita.com/")!) { UIApplication.shared.open(URL(string: "https://qiita.com/")!) } else { print("ブラウザ起動失敗") }// To-BE例 UIApplication.shared.open(URL(string: "https://qiita.com/")!)その2:plistにhttp/httpsを追加する
canOpenURLの判定には、plistの設定が反映されるためブラウザ起動のURLを許可する。
<key>LSApplicationQueriesSchemes</key> <array> <string>http</string> <string>https</string> </array>参考
公式:
UIApplication | Apple Developer Documentation(引用はこちらより)
canOpenURL(_:) | Apple Developer Documentation参考:
特定のアプリがインストール済みかチェックする
[iOS] ディープリンク(Custom URL Scheme)でアプリを起動する
間違っている点や他の方法があれば教えてください!
iOS14の発表でバタバタしている同志のiOSエンジニアのみなさま、お互いに本当にお疲れ様です。免責:全てのiOS14のアプリの動作を保証する解決方法ではございません
- 投稿日:2020-09-16T16:50:13+09:00
iOSの「ショートカット」アプリ(旧Workflow)で、無限ループさせる方法
✏️この記事について
ショートカットアプリを作成している際に、「無限ループ」の方法を見つけたため、そのメモを備忘録がわりに書きます
✨教訓?「無限ループ」は存在します!
iOSのショートカットアプリ、便利で使っていたのですが、唯一for文による無限ループが見当たらないことに不満を持っていました。(「参考」を参考にしてください)
?
でも、どうやら、ショートカットアプリで、「無限ループ」を実現する術があるようです。
無限ループを作成する
「ショートカット」にはアクションのグループを永遠に繰り返すアクションは用意されていませんが、「ショートカットを実行」アクションを使用することでこの動作をエミュレートできます。「ショートカットを実行」アクションを使用すると、ショートカット内で別のショーカットを実行できます。「ショートカットを実行」アクションで内包されている同じショートカットを実行するように構成すると、ショートカットは停止するまで最初から最後まで繰り返し実行されます。
「ショートカット」で「繰り返す」アクションを使用する - Apple サポート
ふむ...「ショートカットの実行」を、ショートカット内に埋め込めばいいんですね
できた...?
ショートカットアプリの幅が広がりそうですね?参考
?作ったレシピ
?「繰り返す」アクション
?➕か、➖ボタンでしか制御できない...
?「各項目を繰り返す」アクション
?改行された文章の行数を数えるなど、基礎的な制御ができる
参考サイト様
- 投稿日:2020-09-16T16:09:01+09:00
XamarinのVisualStudio for Macで実機ビルドができなくなった時の対処の備忘録
参考
Xamarinで作成したiOSアプリが実機デバッグ起動できなくなった場合の対処方法
Visual Studio For MacでのiOS実機デバッグについて
対処
- クリーンしてビルド
- Visual Studio for Macの再起動
- Xcodeの再起動
- iPhoneの再起動
- Developer CenterにてDeviceの登録
- APP idのプロビジョニングプロファイルの作成
今回は「Visual Studio for Macの再起動」で解決。
前回は開発環境に入ってるアプリを削除したら解決したり、Macの環境はわからない。
iPhone7のデバッグの時なんかは同じエラーでiPhone側の空き容量が足りなくて2時間も悩まされた。日本一やさしいMacの手本とか存在しないかしら。
- 投稿日:2020-09-16T15:10:52+09:00
【iOS14】UNUserNotificationCenterでSwitch must be exhaustiveのwarning対応
Xcode12(GM)でビルドすると
Switch must be exhaustive
のwarningが?
実装を確認すると、通知許可ステータスを確認するコードで吐き出されている様子UNUserNotificationCenter.current().getNotificationSettings { settings in switch settings.authorizationStatus { case .authorized, .provisional: print("許可") case .denied: print("拒否") case .notDetermined: print("許可求む") @unknown default: fatalError() }
UNAuthorizationStatus
のリファレンスを覗くとiOS14から新たに
.ephemeral
が追加されていました。
https://developer.apple.com/documentation/usernotifications/unauthorizationstatusこの子は
The application is temporarily authorized to post notifications. Only available to app clips.
つまり、iOS14から実装可能なApp Clipsにおいて、一時的に通知の許可を取ることができ、そのステータスということです。App Clipsを実装している場合の実装はこちらの公式リファレンスを参考に
https://developer.apple.com/documentation/app_clips/enabling_notifications_in_app_clipslet center = UNUserNotificationCenter.current() center.getNotificationSettings { (settings) in { if settings.authorizationStatus == .ephemeral { // The user didn't disable notifications in the app clip card. // Add code for scheduling or receiving notifications here. return } }などを追加します。
App Clipsを実装していない場合は、通らない想定なので
UNUserNotificationCenter.current().getNotificationSettings { settings in switch settings.authorizationStatus { case .authorized, .provisional: print("許可") case .denied: print("拒否") case .notDetermined: print("許可求む") case .ephemeral: fatalError()// or assertionFailure() @unknown default: fatalError() }でwarning回避できます。
リファレンスにbetaと書かれているし、fatalError
利用したくない方は念の為
assertionFailure
で、取得失敗の扱いで良いかと思います?♂️ご参考までに??
- 投稿日:2020-09-16T14:31:44+09:00
ライブラリを使わずに美しいカラーピッカーを実装【iOSアプリ】【Swift】
経緯
お絵かきアプリにカラーピッカーを付けたかったのでいろいろ探したのですが、ライブラリを使ったのしか出てこなかった。
...
...
じゃあ自力で1から作っちゃおう!環境
・macOS Catalina
・Xcode 11.7
・Swift 5Assetsに画像を追加
・千鳥柄の画像 × 2枚
※なくても良い。(alphaが1より低いときにわかりやすくするため)
1枚目 -> サイズ:220 × 37.5 、名前:chidori_s
2枚目 -> サイズ:290 × 290 、名前:chidori
・Hueスライダー用の画像
サイズ:300×13、名前:colors
画像は私のブログ記事から入手できます。全コード
ボタンを押すとColor Pickerが現れます。
import UIKit class ViewController: UIViewController { // 現在表示されているSlider var sliderNow = "" // Hue一時保存用 var hue: CGFloat! // Color Picker を表示するボタン var colorBtnNow: UIButton! // Color Picker var picker: UIView! var sliderPallete: UIView! // Color Pickerの上の白丸 let thumb = UIView() // 変更前、現在の色プレビュー var twoColors = [UIButton(), UIButton()] // パレットのグラデーションレイヤー var gradLayer = [CAGradientLayer(), CAGradientLayer()] // Slider var colorSlider = [UISlider(), UISlider(), UISlider(), UISlider()] // Slider の色 let sliderGrad = [CAGradientLayer(), CAGradientLayer(), CAGradientLayer(), CAGradientLayer()] // 現在のSliderの値 var numText = [UILabel(), UILabel(), UILabel(), UILabel()] // 現在の色 var colorNow: UIColor! // 現在のrgba/hsba var changingColor = [CGFloat(), CGFloat(), CGFloat(), CGFloat()] override func viewDidLoad() { super.viewDidLoad() let b = UIButton() b.frame = CGRect(x: 10, y: 60, width: 80, height: 50) b.backgroundColor = .systemOrange b.setTitle("button", for: .normal) view.addSubview(b) b.addTarget(self, action: #selector(showColorPicker(sender:)), for: .touchUpInside) } @objc func showColorPicker(sender: UIButton) { picker = UIView() picker.backgroundColor = UIColor(white: 0.2, alpha: 1) picker.layer.cornerRadius = 10 picker.frame = CGRect(x: 60, y: 120, width: 310, height: 615) view.addSubview(picker) colorBtnNow = sender let parameters = ["RGB", "HLS"] let seg = UISegmentedControl(items: parameters) seg.frame = CGRect(x: 95, y: 20, width: 120, height: 35) seg.backgroundColor = .gray seg.selectedSegmentIndex = 0 seg.addTarget(self, action: #selector(segmentChanged), for: .valueChanged) picker.addSubview(seg) // 色プレビュー let colorView = UIImageView() colorView.image = UIImage(named: "chidori_s") colorView.frame = CGRect(x: 45, y: 70, width: 220, height: 37.5) colorView.clipsToBounds = true colorView.layer.cornerRadius = 6 colorView.isUserInteractionEnabled = true picker.addSubview(colorView) var x: CGFloat = 0 for c in twoColors { c.frame = CGRect(x: x, y: 0, width: 110, height: colorView.frame.height) c.backgroundColor = sender.backgroundColor! colorView.addSubview(c) c.addTarget(self, action: #selector(setColor), for: .touchDown) x = 110 } // グラデーションパレット let hsb = sender.backgroundColor!.hsb() let gradView = UIButton(frame: CGRect(x: 10, y: 120, width: 290, height: 290)) gradView.addTarget(self, action: #selector(colorGradLayer_tapped), for: .allTouchEvents) gradView.setBackgroundImage(UIImage(named: "chidori"), for: .normal) hue = hsb[0] func set_gradLayer(_ g: CAGradientLayer, colors: [CGColor], start: CGPoint) { g.colors = colors g.startPoint = start g.endPoint = CGPoint(x: 0.0, y: 0.0) g.frame = CGRect(x: 0, y: 0, width: 290, height: 290) g.locations = [0, 1] gradView.layer.addSublayer(g) } set_gradLayer(gradLayer[0], colors: [hsb_color([hsb[0], 1, 1, hsb[3]]).cgColor, UIColor(white: 1, alpha: hsb[3]).cgColor], start: CGPoint(x: 1.0, y: 0.0)) set_gradLayer(gradLayer[1], colors: [UIColor.black.cgColor, UIColor.clear.cgColor], start: CGPoint(x: 0.0, y: 1.0)) thumb.center = CGPoint(x: 290*hsb[1], y: 290*(1-hsb[2])) thumb.frame.size = CGSize(width: 24, height: 24) thumb.layer.cornerRadius = 12 thumb.backgroundColor = .white thumb.isUserInteractionEnabled = false gradView.addSubview(thumb) picker.addSubview(gradView) setUpRGBView() } @objc func colorGradLayer_tapped(sender: UIButton, event: UIEvent) { if let touch = event.touches(for: sender)?.first { // タップした場所を取得 let loc = touch.location(in: sender) if 0..<290 ~= loc.x { thumb.center.x = loc.x } if 0..<290 ~= loc.y { thumb.center.y = loc.y } changingColor[0] = hue changingColor[1] = thumb.center.x/290 changingColor[2] = 1 - thumb.center.y/290 if sliderNow == "hls" { for i in 1...2 { updateSlider(tag: i, value: changingColor[i]) } } else { changingColor = hsb_color(changingColor).rgb() for i in 0...2 { updateSlider(tag: i, value: changingColor[i]) } } changeColor() } } @objc func segmentChanged(_ segment: UISegmentedControl) { switch segment.selectedSegmentIndex { case 0: setUpRGBView() case 1: setUpHLSView() default: break } } @objc func setColor(sender: UIButton) { if sliderNow == "hls" { changingColor = sender.backgroundColor!.hsb() } else { changingColor = sender.backgroundColor!.rgb() } changeColor() } @objc func setUpHLSView() { sliderNow = "hls" setSlider(colors: colorBtnNow.backgroundColor!.hsb(), text: ["colors", "satu", "br", "alpha"]) colorSlider[0].setMinimumTrackImage(UIImage(named: "colors"), for: .normal) colorSlider[0].setMaximumTrackImage(UIImage(named: "colors"), for: .normal) for s in colorSlider[0].layer.sublayers! { s.removeFromSuperlayer() } set_grad_hls(1...3) } func set_grad_hls(_ range: ClosedRange<Int>) { for i in range { var c = changingColor if i != 3 { c[3] = 1 } c[i] = 0 let color1 = UIColor(hue: c[0], saturation: c[1], brightness: c[2], alpha: c[3]) c[i] = 1 let color2 = UIColor(hue: c[0], saturation: c[1], brightness: c[2], alpha: c[3]) sliderGrad[i].colors = [color1.cgColor, color2.cgColor] } } @objc func setUpRGBView() { sliderNow = "rgb" let color = colorBtnNow.backgroundColor! setSlider(colors: color.rgb(), text: ["red", "green", "blue", "alpha"]) set_grad_rbg(0...3) } func set_grad_rbg(_ range: ClosedRange<Int>) { for i in range { var c = changingColor if i != 3 { c[3] = 1 } c[i] = 0 let color1 = UIColor(red: c[0], green: c[1], blue: c[2], alpha: c[3]) c[i] = 1 let color2 = UIColor(red: c[0], green: c[1], blue: c[2], alpha: c[3]) sliderGrad[i].colors = [color1.cgColor, color2.cgColor] } } func setSlider(colors: [CGFloat], text: [String]) { sliderPallete?.removeFromSuperview() sliderPallete = UIView(frame: CGRect(x: 0, y: 410, width: 310, height: 250)) picker.addSubview(sliderPallete) for i in 0..<colors.count { colorSlider[i] = slider(tag: i, img: text[i], value: colors[i], superV: sliderPallete) colorSlider[i].addTarget(self, action: #selector(slider_value_changed), for: .valueChanged) changingColor[i] = colors[i] } for i in 0..<sliderGrad.count { sliderGrad[i].frame = CGRect(x: 0, y: 0, width: 220, height: 20) sliderGrad[i].startPoint = CGPoint(x: 0.0, y: 0.0) sliderGrad[i].endPoint = CGPoint(x: 1.0, y: 0.0) sliderGrad[i].locations = [0, 1] sliderGrad[i].cornerRadius = 10 colorSlider[i].layer.addSublayer(sliderGrad[i]) } } func slider(tag: Int, img: String, value: CGFloat, superV: UIView) -> UISlider { numText[tag].backgroundColor = UIColor.clear numText[tag].font = .systemFont(ofSize: 16, weight: UIFont.Weight(6)) numText[tag].textColor = .white superV.addSubview(numText[tag]) let s = UISlider() s.tag = tag s.tintColor = .clear s.frame = CGRect(x: 65, y: 25+tag*40, width: 220, height: 20) superV.addSubview(s) if img == "alpha" { numText[tag].text = String(format: "%.1f", value) } else { numText[tag].text = "\(Int(value*255))"} s.maximumValue = 0 s.maximumValue = 1 s.setValue(Float(value), animated: false) numText[tag].frame = CGRect(x: 14, y: s.frame.origin.y-9, width: 60, height: 35) return s } @objc func slider_value_changed(slider: UISlider) { updateSlider(tag: slider.tag, value: CGFloat(slider.value)) changeColor() changingColor[slider.tag] = CGFloat(slider.value) let c = changingColor // update grad layer if sliderNow == "hls" { hue = c[0] switch slider.tag { case 0, 3: gradLayer[0].colors = [hsb_color(c).cgColor, UIColor(white: 1, alpha: c[3]).cgColor] set_grad_hls(1...3) case 1: thumb.center.x = 290*c[1]; break case 2: thumb.center.y = 290*(1-c[2]); break default: break } } else { let hsb = rgb_color(c).hsb() hue = hsb[0] thumb.center = CGPoint(x: 290*hsb[1], y: 290*(1-hsb[2])) gradLayer[0].colors = [hsb_color([hsb[0], 1, 1, hsb[3]]).cgColor, UIColor(white: 1, alpha: hsb[3]).cgColor] } } func updateSlider(tag: Int, value: CGFloat) { colorSlider[tag].setValue(Float(value), animated: false) if tag == 3 { numText[tag].text = String(format: "%.1f", value) } else { numText[tag].text = "\(Int(value*255))" } } func changeColor() { var color = rgb_color(changingColor) if sliderNow == "hls" { color = hsb_color(changingColor) } else { set_grad_rbg(0...3) } colorBtnNow.backgroundColor = color colorNow = color twoColors[1].backgroundColor = color } func hsb_color(_ c: [CGFloat]) -> UIColor { return UIColor(hue: c[0], saturation: c[1], brightness: c[2], alpha: c[3]) } func rgb_color(_ c: [CGFloat]) -> UIColor { return UIColor(red: c[0], green: c[1], blue: c[2], alpha: c[3]) } } extension UIColor { func hsb() -> [CGFloat] { var (h, s, b, a) = (CGFloat(), CGFloat(), CGFloat(), CGFloat()) getHue(&h, saturation: &s, brightness: &b, alpha: &a) return [h, s, b, a] } func rgb() -> [CGFloat] { var (r, g, b, a) = (CGFloat(), CGFloat(), CGFloat(), CGFloat()) getRed(&r, green: &g, blue: &b, alpha: &a) return [r, g, b, a] } }実装例
まとめ
スライダーの色を動的に変えることで
直感的に操作できるUIになりましたね!
- 投稿日:2020-09-16T14:00:40+09:00
DZNEmptyDataSetを使わずにEmptyStateを実装する
DZNEmptyDataSetは2014年6月(執筆時点で6年前)からある、EmptyStateを表示するためのライブラリで、今も現役で利用されている素晴らしいライブラリです。
ですが、「ちょっとEmptyStateの表示したいだけなのにライブラリを入れるほどでもないし...」とか「SPMに寄せたいけど、DZNEmptydataSetはSPM対応のPRまだマージしてなくて、stableだとCocoaPodsかCarthageしか使えないし...」とか「Method Swizzlingが怖い...」などなど、自作を試みたい動機がいくつかあると思います。
そこで、簡単に自作する方法を紹介したいと思います。SwiftUIでは
if
で切り分けて表示するだけで、非常に簡単なため今回は扱いません。実装 Delegate
まず、このようにEmptyStateで表示したいラベルやViewを用意します。今回はラベルのみです。
YourViewController.swiftlet emptyLabel: UILabel = { let label = UILabel(frame: .zero) label.font = UIFont.preferredFont(forTextStyle: .title1) label.textColor = UIColor.secondaryLabel label.textAlignment = .center label.text = "Text that you want to display" label.isUserInteractionEnabled = false return label }()そのEmptyState用のViewを、スクリーンの高さと等しいViewに対して追加します。その理由としては、ツールバーなどが下部に含まれていると画面中心より上部にEmptyStateのViewが表示されてしまい(centerが高さ分ズレるため)、ビジュアルに違和感が生じるためです。今回は
navigationController?.view
としました。(ご自分の環境に合わせて適切にViewの選択をお願いします)この実装ではViewDebuggerを見ると、
emptyLabel
が最前面に存在している形になります。YourViewController.swiftnavigationController?.view.addSubview(emptyLabel) navigationController?.view.bringSubviewToFront(emptyLabel) emptyLabel.equalToParent()レイアウトを簡単にするために、いくつか
extension
を追加しています。Constraint.swiftextension UIView { @_functionBuilder public struct ConstrainsBuilder { static func buildBlock(_ constraints: NSLayoutConstraint...) -> [NSLayoutConstraint] { constraints } } public func makeConstraints(@ConstrainsBuilder builder: (UIView) -> [NSLayoutConstraint]) { translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate(builder(self)) } /// - Precondition: The superView should not be nil. public func equalToParent() { precondition(superview != nil) self.makeConstraints { $0.leadingAnchor.constraint(equalTo: superview!.leadingAnchor) $0.trailingAnchor.constraint(equalTo: superview!.trailingAnchor) $0.topAnchor.constraint(equalTo: superview!.topAnchor) $0.bottomAnchor.constraint(equalTo: superview!.bottomAnchor) } } }あとはテーブルやコレクションのデータソースで出し分けをするのみです。今回はテーブルの実装を解説します。
データソースの変更の際に呼ばれる
tableView(_:numberOfRowsInSection:) -> Int
の内部で、その時返すデータ数に応じて、isHidden
の切り替えを行うと期待する挙動をします。YourViewController+DataSource.swiftextension YourViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let numberOfRows = yourItems.count ?? 0 if numberOfRows == 0 { emptyLabel.isHidden = false } else { emptyLabel.isHidden = true } return numberOfRows } }まとめ
多少ソースコードに余分を含むことにはなりますが、ライブラリを使わなくてもデータ件数による切り替えを行うだけで
EmptyState
の実現ができました。PS
Combine
Publisher
を使えば、実装もうちょい楽にできないかな?と思い以下のようにしてみたのですが、データを変更してみても一回しか値が流れてこなかったので諦めました。Combine
はまだ素人のため、何か実現方法あれば教えていただけると助かります。Publisher.swifttableView.publisher .receive(on: RunLoop.main) .map({ $0.numberOfRows(inSection: 0) > 0 }) .assign(to: \.isHidden, on: emptyLabel) .store(to: &cancellables)
- 投稿日:2020-09-16T10:08:42+09:00
UICollectionViewCompositionalLayoutでWaterfall(Pinterest風)レイアウトを実現する
iOS 13が登場して1年が経過しました。(もうiOS 14も出ますね)
昨年登場した物としてSwiftUIやCombineは注目度が高かったですがUICollectionViewCompositionalLayout
も忘れてはいけない存在です。
UICollectionViewCompositionalLayout
は簡単に柔軟なレイアウトを構築することができるので使い始めるとかなりの画面で役に立ちます。
しかし、少し凝ったレイアウトをしようと思うとどうすればいいのか分からなかったので、今回はPinterest風のレイアウトをどう実現するのかを考えてみました。Waterfallレイアウト(Pinterest風レイアウト)とは
Waterfallレイアウトはこのように上からサイズが異なるコンポーネントが上から積み重なっているようなレイアウトのことを言います。
水が上から下に落ちるように見えるのでWaterfall(滝)ということですね。レイアウトの考え方
レイアウトを作る際に重要なのは次の2点です。
- カラム数(横に何列表示するか)
- それぞれのセルのサイズが明確なこと
上記が分かっていればカラムごとに高さを足していけばそれっぽいレイアウトが作れます。
実装サンプル
protocol WaterfallLayoutDelegate: AnyObject { func numberOfColumns() -> Int func columnsSize(at indexPath: IndexPath) -> CGSize func columnSpace() -> CGFloat } lazy var collectionViewLayout: UICollectionViewCompositionalLayout = { return UICollectionViewCompositionalLayout { [unowned self] (section, environment) -> NSCollectionLayoutSection? in let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(environment.container.effectiveContentSize.height)) let group = NSCollectionLayoutGroup.custom(layoutSize: groupSize) { [unowned self] (environment) -> [NSCollectionLayoutGroupCustomItem] in var items: [NSCollectionLayoutGroupCustomItem] = [] var layouts: [Int: CGFloat] = [:] let space: CGFloat = self.waterfallLayout.flatMap({ CGFloat($0.columnSpace()) }) ?? 1.0 let numberOfColumn: CGFloat = self.waterfallLayout.flatMap({ CGFloat($0.numberOfColumns()) }) ?? 2.0 let defaultSize = CGSize(width: 100, height: 100) (0 ..< self.collectionView.numberOfItems(inSection: section)).forEach { let indexPath = IndexPath(item: $0, section: section) let size = self.waterfallLayout?.columnsSize(at: indexPath) ?? defaultSize let aspect = CGFloat(size.height) / CGFloat(size.width) let width = (environment.container.effectiveContentSize.width - (numberOfColumn - 1) * space) / numberOfColumn let height = width * aspect let currentColumn = $0 % Int(numberOfColumn) let y = layouts[currentColumn] ?? 0.0 + space let x = width * CGFloat(currentColumn) + space * (CGFloat(currentColumn) - 1.0) let frame = CGRect(x: x, y: y + space, width: width, height: height) let item = NSCollectionLayoutGroupCustomItem(frame: frame) items.append(item) layouts[currentColumn] = frame.maxY } return items } return NSCollectionLayoutSection(group: group) } }()大事なところだけ抜き出しておきました。
フルのサンプルコードは以下のGistに置いています。まとめ
NSCollectionLayoutGroup.custom
を利用することで柔軟なレイアウトも簡単に構築することができました。
NSCollectionLayoutGroupCustomItem
はframe
の他にもzIndex
を指定することができるので、色々と柔軟なレイアウトが作れそうですね。
- 投稿日:2020-09-16T05:28:07+09:00
FlutterでiOS、Android両方で動くカメラアプリを作る
Flutterを使ってマルチプラットフォームなカメラアプリを作っていく
「iOSでもAndroidでも動くアプリで、」
「スマホ内の画像にアクセスしてローカルのDBに保存可能」
「アプリ内でもカメラを起動できる」っていう要件をFlutterで満たせるかの検証をバイト先に任されたからなー
Flutter
iOS、Androidといった異なるプラットフォームのアプリを一つのコードで書くことができるGoogle製のオープンソースのクロスプラットフォームのフレームワーク
公式ドキュメントも充実・サードパーティー製のプラグインも日々増えているので、旬な技術だと思っている
公式Youtubeにはお洒落な感じデザインのやらWidgetやらの説明動画がたくさんあるので楽しい
環境構築は以下のサイトを参考にした。ありがとうございました
ホットリロードが神すぎる
早速作っていきたいのだけど、Flutterは言語はdartでWidget単位でコードを完成させてアプリを作っていく
公式ドキュメントでもFlutterではWidgetが全てだよって言ってる
In Flutter, “everything is a widget”! - Flutter documentation
特に重要なWidgetが Stateless Widget と Statefull Widget の2つで順番におさらいしていく
そのあと、画像ファイル・カメラへのアクセスを可能にするプラグインであるimage_picker と、データベースを扱うためのプラグインsqfliteの紹介に入って、最後にコードを説明して終わり
Stateless Widget
名前の通り状態を持たないWidget。
以下は公式YouTubeで取り上げられているコード例
class ItemCount extends StatelessWidget { final String name; final int count; ItemCount({this.name, this.count}); @overide Widget build(BuildContext context) { return Text('$name: $count') } }String型のnameとint型のcountを与えると、それらをTextWidgetに渡して表示させるようなWidgetである。
Statefull Widget
状態をもつWidgetで、Stateが更新されたときに内容が更新される。
先ほどのコードをStatefulにすると以下のような感じになる。
class ItemCount extends StatefulWidget { final String name; ItemCounter({this.name}); @override _ItemCounterState createState() => _ItemCounterState(); } class _ItemCounterState extends State<ItemCounter> { int count = 0; @overide Widget build(BuildContext context) { return GestureDetector( onTap: () { setState(() { count++; }); ), child: Text('${widget.name}: $count'), ); } }setStateはその中に書かれた変数が変化した時にクラスに変更を伝える役割を持っている
これでStatelessだった画面が、タップするとcountが上がっていくプログラムになった
image_picker
image_pickerははアプリ内でカメラを立ち上げたり、スマホ内のメディア(画像)にアクセスさせたりするのに使うプラグイン
A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. - image_picker | pub.dev
フラッター開発チームが作った純正なのでいろいろ安心
Flutterでのプラグインの導入は
pubspec.yaml
にプラグインの情報を追加して$ flutter pub get
してあげるだけこのプラグインではフォトライブラリへのアクセスを許可しないといけないので、iOSでは以下の変数に関する設定を
ios/Runner/info.plist
に事前に書き込んでおく必要がある
変数名 役割 NSPhotoLibraryUsageDescription
フォトライブラリへのアクセス NSCameraUsageDescription
カメラへのアクセス NSMicrophoneUsageDescription
マイクへのアクセス 動画を取り込まないなら一番下の項目は無視できる
細かい設定の仕方は公式ドキュメントをご参照ください
公式ドキュメントをサンプルのコードに手を加え、ペッとmain.dartに貼り付けたら動くものを作った
動作環境はMacBookPro上の
android-x86 emulator(Pixel_3a_API_30_x86)
である例えばカメラの起動と写真の取得はたったこれだけ。
カメラの起動・写真の取得final pickedFile = await picker.getImage(source: ImageSource.camera);
ImageSource.gallery
ならフォルダへのアクセスができるかなり使えそう。
sqflite
SQLite(データベース)を導入・利用するためのプラグイン
Flutter plugin for SQLite, a self-contained, high-reliability, embedded, SQL database engine. - sqflite | pub.dev
導入は例に漏れず
pubspec.yaml
にプラグインの情報を追加して$ flutter pub get
してあげるだけ公式Githubとか他の記事見ながら理解した
主な関数の説明を列挙していく
まずはデータベースへのパスを取得する必要があるらしいので、
import 'package:path/path.dart'
しておいて、getDatabasesPath関数でパスを取得するそれを用いると、データベースを開く処理はこんな感じでできる
openDatabase関数String _path = join(await getDatabasesPath(), "mydb.db"); Database database = await openDatabase(_path, version: 1, onCreate: 処理);新規にデータベースを作成してアクセスする時などは、openDatabase関数実行時にonCreateを設定すれば良い
onCreate関数は、データを事前準備しておきたい時にも使える
onCreate関数String _path = join(await getDatabasesPath(), "mydb.db"); Database _database = await openDatabase(_path, version: 1, onCreate: (Database db, int version) async { await db.execute( "CREATE TABLE IF NOT EXISTS mydb (id INTEGER PRIMARY KEY, text TEXT)"); // データを事前に流し込みたい時はここにinsertを記述すれば良い });データベースの内容の書き換えなどで、transaction関数を利用できる
transaction関数await _database.transaction((txn) async { await txn.rawInsert('INSERT INTO mydb(text) VALUES("$textData")'); });レコードの取得にはrawQuery、保存にはrawInsert、更新はrawUpdate、削除はrawDeleteを使えば良い
rawQuery関数の結果はMap型のリストで返ってくるrawQuery関数List<Map> _result = await _database.rawQuery('SELECT * FROM mydb');SQLとかを発行しない処理であれば、execute関数で行える
本題
カメラとギャラリーから画像を取ってきて、DB(SQLite)に保存するアプリを作ってみた。
まずは完成したアプリをみて欲しい.
Androidエミュレーターでの動き
iPhoneエミュレーターでの動き
Android、iphoneの両方で動作するアプリがであることが確認できる(ただし、iphoneエミュレーター上ではカメラは起動しない)
sqfliteに画像を保存する場合は画像情報をbase64でエンコードしたテキストとして写真の実体を保存する
自分はこちらを参考にした(Thank you!)
Google’s Flutter Tutorial – Save Image as String in SQLite Database
画像を保存しておくためのテーブルは以下のように定義している
initDB() async { io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); String path = join(documentsDirectory.path, DB_NAME); var db = await openDatabase(path, version: 1, onCreate: _onCreate); return db; } _onCreate(Database db, int version) async { await db.execute("CREATE TABLE $TABLE ($ID INTEGER, $DATA TEXT)"); }ギャラリーからピックアップしてきた画像をBase64でエンコードして保存
_pickImageFromGallery() { final picker = ImagePicker(); picker.getImage(source: ImageSource.gallery).then((imgFile) async { if (imgFile != null) { String imgString = Utility.base64String(await imgFile.readAsBytes()); Photo photo = Photo(0, imgString); dbHelper.save(photo); } }); }完成したコードはGitHubにおいておきます
- 投稿日:2020-09-16T00:11:40+09:00
StoryboardまたはコードでUITableViewを扱う
Storyboardでのやり方と、コードで生成するやり方をまとめてみます
UITableViewController
には触れてないですStoryboardで
UITableView
を配置して、outlet接続しますUITableViewCell
を配置して、識別子をつけますUITableViewCell
のスタイルを設定しますcustom
にする場合は、UITableViewCell
を継承したカスタムクラスを作る必要があります
そしてUITableViewCell
に必要なものを配置しますdatasource
を用意して、セルを生成します(下は簡単な例)class ViewController: UIViewController { @IBOutlet weak var myTableView: UITableView! override func viewDidLoad() { super.viewDidLoad() myTableView.dataSource = self } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = myTableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = "\(indexPath.row)" return cell } }コードのみで
ViewController
にUITableView
を追加しますclass ViewController: UIViewController { var myTableView = UITableView() override func viewDidLoad() { super.viewDidLoad() // myTableViewのサイズ設定は必須。frameプロパティで設定しても、autoLayoutで設定しても良い self.view.addSubview(myTableView) } // myTableViewのサイズをautoLayoutで設定する override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() myTableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ myTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0), myTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0), myTableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0), myTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0) ]) } }
- セル用の識別子を登録します
custom
にする場合は、UITableViewCell
を継承したカスタムクラスを作る必要があります
そしてUITableViewCell.self
をカスタムクラス.self
に変えますoverride func viewDidLoad() { // 省略 myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") // 省略 }
datasource
を用意して、セルを生成しますclass ViewController: UIViewController { var myTableView = UITableView() override func viewDidLoad() { // 省略 myTableView.dataSource = self } // myTableViewのサイズをautoLayoutで設定する override func viewWillLayoutSubviews() { // 省略 } } // 以下のコードはStoryboardの時と同じ extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = myTableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = "\(indexPath.row)" return cell } }xibファイルを使う
xibファイルを使う場合、カスタムセルを利用しなければならないです
File > New > File...
またはcommand N
で新しいセル用のクラスを作ります
xibファイルも一緒に作るのでチェックを入れます
xibファイル内のViewのclass欄にカスタムセルのクラスを指定します。(自動生成の場合は自動で指定してくれます)
xibファイルのViewに配置したものを、カスタムセルのクラスにoutlet接続します
あとは上のコードのみの時のコードを少し変えれば動きます
class ViewController: UIViewController { var myTableView = UITableView() override func viewDidLoad() { super.viewDidLoad() // myTableViewのサイズ設定は必須。frameで設定しても、autoLayoutで設定しても良い self.view.addSubview(myTableView) // myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") myTableView.register(UINib(nibName: "myCustomCell", bundle: nil), forCellReuseIdentifier: "xibCell") myTableView.dataSource = self } // myTableViewのサイズをautoLayoutで設定する override func viewWillLayoutSubviews() { // 省略 } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // let cell = myTableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) // cell.textLabel?.text = "\(indexPath.row)" let cell = myTableView.dequeueReusableCell(withIdentifier: "xibCell", for: indexPath) as! myCustomCell cell.leftLabel.text = "\(indexPath.row)" cell.centerLabel.text = "\(indexPath.row)" cell.rightLabel.text = "\(indexPath.row)" return cell } }最後に
Storyboardとxibファイルを使わず、
完全にコードでUITableView
とカスタムセルを実装する場合は、
コードの記述量がたくさん増えるのがわかると思います。
特にAutoLayoutをコードで書けば非常に長くなります。
どう使い分けるべきかよくわからないですが、
もしコメントで教えていただければ幸いです。参考記事