- 投稿日: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
【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-16T20:16:43+09:00
iOS14GMでcanOpenURLがおかしいから改めて整理してみた
何が起こっている?
iOS14 PublicBeta8以降(〜iOS14 GM版)を入れたiOS端末で、デフォルトブラウザを変更した際にブラウザ遷移が行えない
canOpenURL returns false when I changed the default browser on iOS 14発生条件
- iOS14
- デフォルトブラウザをsafari以外に設定していること
デフォルトブラウザ設定方法
- 「設定」アプリ起動
- デフォルトブラウザにしたいアプリへ移動(例:Chrome)
- 「デフォルトブラウザApp」を開く
- 標準ブラウザをSafariからChromeに変更する
原因
UIApplicationの
canOpenURL
メソッドがfalseを返却している。
これが仕様なのかバグなのかは現時点(日本時間2020/9/16)では不明そもそもcanOpenURLとは?
アプリから別のアプリを起動する(Custom URL Scheme)際に、開こうとしているアプリが存在するかを返す関数。
参考:特定のアプリがインストール済みかチェックする
[iOS] ディープリンク(Custom URL Scheme)でアプリを起動する対応方法
その1:canOpenURLを通さない
そもそもiOS端末にはブラウザアプリとしてSafariが入っており、かつ削除もできないので、
ブラウザ起動をしようとしてアプリがないことは有り得ない はず。
(だから、ブラウザ起動でcanOpenURLがfalseを返すことはないはずなのに今回起こっているわけですが)
そのため、ブラウザ起動のためのURL起動の場合は、canOpenURLを通さずにいきなりopenを叩くように修正する。// AS-IS if UIApplication.shared.canOpenURL(URL(string: "https://google.co.jp")!) { UIApplication.shared.open(URL(string: "https://google.co.jp")!) } else { print("ブラウザ起動失敗") }// To-BE UIApplication.shared.open(URL(string: "https://google.co.jp")!)その2:plistにhttp/httpsを追加する
canOpenURLの判定には、plistの設定が反映されるためブラウザ起動のURLを許可する。
<key>LSApplicationQueriesSchemes</key> <array> <string>http</string> <string>https</string> </array>
間違っている点や他の方法があれば教えてください
- 投稿日: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-16T19:32:07+09:00
Swiftでデザインパターン【Decorator】
元ネタ → ochococo/Design-Patterns-In-Swift
クラス図
図の引用元:Wikipedia: Decorator パターン
概要
The decorator pattern is used to extend or alter the functionality of objects at run- time by wrapping them in an object of a decorator class. This provides a flexible alternative to using inheritance to modify behavior.
Decoratorパターンは、Decoratorクラスのオブジェクトでオブジェクトをラップすることにより、実行時にオブジェクトの機能を拡張または変更するために使用する。これは、継承を使用して動作を変更する代わりに柔軟な方法を提供する。サンプルコード
// Component protocol CostHaving { // operation var cost: Double { get } } // Component protocol IngredientsHaving { // operation var ingredients: [String] { get } } // Component typealias BeverageDataHaving = CostHaving & IngredientsHaving // ConcreteComponent struct SimpleCoffee: BeverageDataHaving { // operation let cost: Double = 1.0 let ingredients = ["Water", "Coffee"] } // Decorator protocol BeverageHaving: BeverageDataHaving { // component var beverage: BeverageDataHaving { get } } // ConcreteDecorator struct Milk: BeverageHaving { // component let beverage: BeverageDataHaving // operation var cost: Double { return beverage.cost + 0.5 } // operation var ingredients: [String] { return beverage.ingredients + ["Milk"] } } // ConcreteDecorator struct WhipCoffee: BeverageHaving { // component let beverage: BeverageDataHaving // operation var cost: Double { return beverage.cost + 0.5 } // operation var ingredients: [String] { return beverage.ingredients + ["Whip"] } } // usage // // 型推論に任せずに明示的に型を決定している。これによって、MilkもWhipCoffeeも同等に扱えるようにしている。 var someCoffee: BeverageDataHaving = SimpleCoffee() print("Cost: \(someCoffee.cost); Ingredients: \(someCoffee.ingredients)") someCoffee = Milk(beverage: someCoffee) print("Cost: \(someCoffee.cost); Ingredients: \(someCoffee.ingredients)") someCoffee = WhipCoffee(beverage: someCoffee) print("Cost: \(someCoffee.cost); Ingredients: \(someCoffee.ingredients)")クラス図との対応
サンプルコード クラス図 CostHaving Component cost operation
サンプルコード クラス図 IngredientsHaving Component ingredients operation
サンプルコード クラス図 BeverageDataHaving Component
サンプルコード クラス図 SimpleCoffee ConcreteComponent cost, ingredients operation
サンプルコード クラス図 BeverageHaving Decorator beverage Component
サンプルコード クラス図 Milk, WhipCoffee ConcreteDecorator beverage component cost, ingredients operation 考察
SimpleCoffe
とMilk/WhipCoffe
に同じプロトコルBeverageDataHaving
を準拠させ、同じ型のインスタンスとして扱っている(ポリモーフィズム)。
someCoffee
は、はじめはSimpleCoffee
のイニシャライザで生成され、その後、Milk
、WhipCoffee
のイニシャライザで置き換えられているが、BeverageDataHaving
型としてアップキャストされているので、すべて同じ型として扱うことができる。
また、Milk
、WhipCoffee
ともにbeverage
を持つことで(集約)、デフォルトの値を参照している。
- 投稿日:2020-09-16T18:44:45+09:00
SceneKitでobjファイルで読み込んだオブジェクトにテクスチャを貼る
はじめに
Structureセンサでスキャン後に書き出したデータ(
.obj
ファイル)を、下記の記事を参考にSceneKit
で読み込んだところ、テクスチャが反映されなかった1。
参考記事:SceneKit to show 3D content in Swift 5
.mtl
を上書き
.mtl
ファイルの記述が足りないようなので、.mtl
ファイルを開き、map_Ka xxx.jpg
を書き足すとテクスチャが表示された(xxx.jpg
はテクスチャ画像)。
なので、.mtl
ファイルの上書きをすることでテクスチャを表示することはできる。コードでテクスチャ画像を指定する
SceneKitのオブジェクトでテクスチャを設定することもできる。
上記参考記事では、SCNScene
のイニシャライザで.obj
ファイルを読み込んでいる。let scene = SCNScene(named: "xxx.obj")しかし、
SCNScene
だと、テクスチャを貼る方法がない(と思われる)。
なので、オブジェクト用のSCNNode()
を用意して、これにテクスチャを貼ることにした。// Sceneに直接オブジェクトを貼るとテクスチャをイジれないので、チャイルドノードとして持たせる let modelObj = SCNMaterial() // objファイル読込(URL) // アプリのディレクトリにファイルを置いている想定 if let documentDirectoryFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last { let fileName = "hoge/xxx.obj" // .objファイル let url = documentDirectoryFileURL.appendingPathComponent(fileName) let objNode = SCNNode(mdlObject: MDLAsset(url: url).object(at: 0)) objNode.geometry?.materials = [modelObj] scene.rootNode.addChildNode(objNode) } // diffuse.contentsにテクスチャを指定する // テクスチャファイル読込(path) if let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last { let fileName = "/hoge/xxx.jpg" // テクスチャファイル modelObj.diffuse.contents = UIImage(named: path+fileName) }参考:ARKitで立方体の6面それぞれに異なるテクスチャを貼る方法
Srructure SDKのサンプルコードはOpen GLで描画している。また、保存した
.obj
ファイルを詠み込む方法は示されていない。Open GLを自力で書いていくのは避けたいため、SceneKit
に逃げたのであった。 ↩
- 投稿日:2020-09-16T15:57:27+09:00
[Swift]3DモデルをSCNSceneに変換する方法2つ
Swiftで3DモデルをSCNSceneに変換するにはModelIOを経由する方法とscnファイルを作成する方法の2つがあります。
片方を実装するともう片方を忘れることが多くてその度に調べることになっているので、備忘録として残しておきます。前提条件は以下の通りです。
- Swift 5
- Xcode 11.7
- iOS 13.7
ModelIOを経由する方法
ModelIOのMDLAssetを使い、そこからSCNSceneを読み込みます。
最初にModelIOをimportしておきましょう。注意点としては本体ではなくSceneKitの方にあるものをimportすることです。example.swiftimport SceneKit.ModelIO変換したい3Dモデルはプロジェクトに追加しておきます。
階層はどこでもいいですが複数の3Dモデルを使うのであればちゃんと1つのディレクトリにまとめておく方がいいでしょう。
今回は"hoge.usdz"という3Dモデルを追加しているものとしています。
あとはBundleから3DモデルのURLを取得して、そのURLをもとにMDLAssetを作成し、作成したMDLAssetからSCNSceneに変換します。example.swiftguard let url = Bundle.main.url(forResource: "hoge", withExtension: "usdz") else { return } let asset = MDLAsset(url: url) let scene = SCNScene(mdlAsset: asset)URL取得時のforResourceにはファイル名を、withExtensionには拡張子を指定します。
今回はusdz形式ですがobjでもgltfでも大丈夫です。scnファイルを作成する方法
コードで実装せず最初からscnファイルを作成しておく方法です。
まずはModelIOを経由する方法と同じように3Dモデルをプロジェクトに追加します。
その後追加した3Dモデルを選択した状態で、Xcode上部のメニューにあるEditorから Convert to SceneKit file format (.scn)を実行します。
するとプロジェクトに3Dモデルのファイルと同名の.scnファイルが追加されます。
例)hoge.usdz → hoge.scn
このscnファイルをSCNSceneで直接読み込みます。example.swiftlet scene = SCNScene(named: "hoge.scn")どちらの方法でも難しいところはありませんが、ModelIOをimportしたりMDLAssetを作成する手間を考えると、scnファイルを作成する方法が便利ではないかと思います。
- 投稿日: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-16T11:43:48+09:00
iOSアプリ開発に必要なスキルをググりまくった
どうも、ねこきち(@nekokichi1_yos2)です。
未経験からiOSエンジニアとしての実力を身に付けるために、とりあえずアプリをリリースすればいいと最近まで思っていました。
ですが、とあるツイートがきっかけで、ポートフォリオを充実させるのではなく、スキルセットを揃えることも重要だと知りました。
↓
フリーランスの面談ではポートフォリオより、スキルシートの方が重要。
— COM@FREELANCE (@com_y_0) August 29, 2020
昔面談前、エージェントにGitHubのURLを教えて欲しいと言われて、1年間なにもPushしてないアカウント送った。
面談でそのことには一切触れられず、普通に合格した。
絶対ポートフォリオみてない。笑確かにエンジニアの転職ではポートフォリオが重要と言われてますが、
(どんなに良い制作物を揃えても、転職先が求める人材、つまり求められるスキルを持ってないと転職は簡単じゃないのでは?)
とも言えます。そもそも、ポートフォリオとはいえ、
・いくつ作ればいい?
・どのような内容が良いのか?
・どんなスキルが望ましいのか?
など、必要なことを挙げればキリがありません。ポートフォリオだけに依存するのではなく、スキルセットにも目を向けるべきだなぁと。
ゴールを一から決めておけば、進む道が見えてくるので。
ということで、iOSアプリ開発、もしくはiOSエンジニアで求められるスキルを片っ端から調べて、自分なりにまとめてみました。
言語
Swift
- 2014年に誕生
- Appleが開発
- モダンでパワフルなオープンソース言語
- iOSのネイティブアプリを開発できる
- Apple製品(Mac,AppleTV,AppleWatch,iPod)のアプリを開発できる
- Objective-Cよりも、学習コストが低く、パフォーマンスでも優れている
- 直感的で、書きやすくて、書くコードの量が少なく、初学者におすすめ
フレームワーク、API
SwiftUI
- WWDC2020で登場
- SwiftよりもUIの開発を抽象化したフレームワーク
- ビルド要らずで、リアルタイムにコードのプレビューが見られる
- コードの依存関係を自動で監視し、UIや値の更新が楽になる
- iOS12以下は非対応
- 複雑なUIや機能の実装には不向き
Alamofire
- API通信用のライブラリ
- 同じ系統のライブラリでは有名
- データ通信の処理を簡単に書ける
- Swiftに搭載されたURLSessionと比較されがち
RxSwift
- FRP(関数型リアクティブプログラミング)のフレームワーク
- オープンソース
- RP:非同期処理、値が変化する度の処理をシンプルに書けるプログラミング
- (Rx系のフレームワークは)エンジニアの必須技術
- GCDやDispatchなど、非同期処理を簡潔に書ける
- 学習コストが高く、チームに導入するのが面倒
Combine
- FRPのフレームワーク
- WWDC2019で登場
- Appleの公式フレームワーク
- RxSwiftと比較されがち
- iOS13以降でないと使用できない
Vapor
- Swiftでサーバーサイド開発ができるフレームワーク
- GoやRailsよりも処理速度が高い
- さくっとサーバーサイドを利用するなら、FirebaseやRealm
- 独自のサーバーサイドを構築するのに向いてる
XCTest
- XcodeのTDD(ユニットテスト)フレームワーク
- UIや機能が想定どおりに動作するかテスト
- 似たフレームワークでQuickがある
- UIをテストできるXCUITestもある
- リファクタリングと併用すれば、コードの安全性が保証される
- テストコードの記述、改修の手間が増える
アーキテクチャ
MVC(Model View Controller)
- Model:データ全般の処理
- View:UIやデータの出力を担当
- Controller:ユーザーからの入力を処理、ViewとModelを制御
- 学習コストが低く、少人数規模向け
- 機能別に分割されているので開発がスムーズ
- 各機能の依存度が高く、テスト、再利用が難しい
CocoaMVC
- Appleが推奨するMVC
- 従来のMVCと違い、ViewとModelが完全に独立
- ControllerがViewとModelを仲介
- Controllerのコードが肥大化し、テストが難しくなる
MVP(Model View Presenter)
- Model:データ処理全般
- View:Modelの監視、Delegateを送る
- Presenter:View,Modelの制御
- データとUIの更新が連動してるので、フローを追いやすい
- MVCよりも学習コストが高く、書くコードが多い
MVVM(Model View ViewModel)
- Model:データ処理全般
- View:UIを表示、ユーザーの入力を受け取る
- ViewModel:Viewと自身を制御
- ViewとViewModelの関係が密で、効率よく更新と表示を処理
- 大規模向けで、学習コストが高く、個人開発向けでない
- RP(RxSwift,ReactiveSwift)のフレームワークを理解する必要があり
データベース
CoreData
- AppleのmBaaS
- 保守性や安定性に優れている
- XcodeとSwiftと相性が良い
- Swiftでしか使えない
- 学習コストが高い
Realm
- オープンソースのmBaaS
- Swift,Kotlin,Java,C#、などに対応
- CoreDataよりも処理速度が早い
- UserDefaultと同じくらい書きやすい
- マルチプラットフォーム開発でも使用可
- オープンソースなので、メンテナンスや改善の手間がかかる
CloudKit
- AppleのmBaaS
- iCloudにデータを保存
- CoreDataと連携可能(ただしiOS13以降)
- 他ユーザーの情報を行き来させるアプリの開発に不向き(チャットアプリなど)
- iOS,Mac間でデータ共有したい人向け
Firebase
- GoogleのmBaaS
- C++,Java,Unityなどにも対応
- リアルタイムのデータ共有
- 多機能で拡張性が高い
- 大規模な開発では、柔軟性が不十分
- 学習コストが高い
ツール、エディタ
Xcode
- Appleが提供するSwift用の統合開発環境(IDE)
- Playgroundにより、リアルタイムでコードを実行できる
- StoryBoardにより、ドラッグ&ドロップでインターフェースを構築できる
- AppStoreにiOSアプリをリリースするのに必要不可欠
- コードをその場で手軽にテストできる
- Gitと連携して効率的なソース管理が可能
ターミナル
- CUI(文字で操作する)ツール
- 主な用途は、ファイル操作、Git
- カーネル(パソコンの重要部分)を安全な形でいじれる
- マウスで行っていた操作をコマンドですぐ実行できる
- プログラミングも可能だが、IDEの専用エディタが大半
Git/GitHub
- Git:ソースコードの変更履歴を分散管理するツール
- GitHub:GitをSNSのように、誰もが閲覧、利用できるサービス
- ソースコード管理ツールはエンジニアの必須技術
- GitHubはGitの派生サービスで、似たサービスは複数ある(GitLab,BitBucket)
- ユーザー数は業界で世界1位
- MSに買収されて、GitLabの台頭を許した
- 全機能を利用するのに料金がかかる
- プライベートリポジトリが有料
GitLab
- GitHubのライバル
- GitLab.com(Webサービス)、CEとEE(オープンソース)の3種類
- 自社サーバー、クラウド(AWS,GCP)での運用が可能
- プライベートリポジトリが無償
- チーム開発での利用が何人でも無償
- Gitリポジトリのimport/exportが可能
- 自社サーバーにセルフホストできて、セキュリティ性が高い
- DevOps機能が圧倒的に充実している
- GitLabの関連ツールを含めた、アップグレードが面倒
CocoaPods
- パッケージ管理ツール
- 使いやすく、多様な操作が可能
- 対応してるライブラリの数が多い
- ライブラリの導入、ビルドに時間がかかる
- Xcodeの使用に依存するので、エラーが起こりやすい
- さくっとライブラリを使いたい人向け
Carthage
- パッケージ管理ツール
- CocoaPodsよりも軽量でビルドが高速
- 依存度が少なく、エラーが起こりにくい
- 手動で設定する必要があり、導入に手間取る
- CocoaPodsよりもライブラリの対応数は少ない
- 安定して柔軟に開発したい人向け
SwiftPackageManager
- Appleの公式パッケージ管理ツール
- CocoaPod,Carthageより新しい
- 他管理ツールに比べ、独立性が高い
- 最新のツールなので、対応ライブラリ数が少ない
- CUIやバックエンドのアプリが得意
CI/CDツール
Bitrise
- クラウド型
- CircleCIと比較される
- 公式サイトは英語
- モバイルアプリ開発向け
- 料金プラン:個人40ドル、チーム100ドル、300ドル
- GUIで操作するので分かりやすい
- iOSに特化しており、Xcodeと相性が良い
- GitHub,GitLab,BitBucketをサポート
CircleCI
- クラウド型
- Bitriseと比較される
- Bitriseと違い、メモリに上限がある
- 料金プラン:無料、30ドル、カスタム
- SaaSで運用コストが低い
- YAML(用途は設定やデータ処理)で記述
- ssh接続によるデバッグ、テスト
- GitHub,BitBucketをサポート
fastlane
- CUI
- モバイルアプリ用の自動化ツール
- ビルド、テスト、デプロイなどを自動化
- リリース作業(証明書、ipa、の設定や作成)も自動化
- アクションと呼ばれる機能を組み合わせて使用
- iTunesConnectで載せるアプリの情報を遠隔で管理できる
SwiftLint
- Realmの静的解析ツール
- Lint:コードをチェックするプログラム
- Swiftのコーディングスタイルに強制する
- バグに繋がる良くないコードを見つけてくれる
- 警告やエラーで知らせてくれる
Danger
- 自動コードレビューツール
- CIツールとの連携が可能
- プラグインが充実している
- PullRequestにコメントでレビュー結果を表示
- PullRequestでのレビューに特化
- Swiftに特化したDanger-Swiftがある
reviewdog
- 自動コードレビューツール
- 機能はDangerとほぼ同じ
- CircleCiなどのCIツールに対応
- linter(SwiftLintなど)とも連携が可能
- 1行単位でのレビューなど、Dangerよりも優れる
サービス
Sketch
- UIデザインツール
- 世界でシェア1位
- 年額$99、無料プランは30日間
- 他デザインツールとの互換性あり(AdobeXD,Figma,Studio)
- 歴史が長く、完成度が高い
- プラグインの数が圧倒的
- プロトタイピングには不向き
AdobeXD
- UIデザインツール
- 日本でシェア1位
- 年額¥14000、無料プランでも大半の機能を使える
- 日本語のチュートリアルが充実している
- プロトタイピングのアニメーションが優れている
- シンプルなUIデザイン、個人開発などに向いてる
- デザインの表現機能がSketchと比べて、イマイチ
Slack
- チャットツール
- ChatWork(日本発祥)と比較される
- 月額¥960,年契約で月額¥850
- 無料プランでも十分に使える
- ワークスペースを複数作成できる
- GitHubやDropboxなどのクラウドサービスと連携できる
- 返信がスレッドで表示されるので、見やすい
- ChatWorkはコスパ、Slackはカスタマイズ性に優れてる
まとめ
本記事を書くまで、全く知らなかった技術ばかりで、以前の無知な自分が恥ずかしく思えてきました。
エンジニアの技術は、”扱う言語で実装できる範囲と深さ”、で決まると思ってましたが、
- ツール
- ライブラリ、フレームワーク
- データベース
- アーキテクチャ
を最適な形で使いこなすことも必要なんだと知りました。利用できる技術の幅が広ければ、開発できるアプリの幅も広がる。
また、エンジニア転職する際も、扱える技術が多ければ、即戦力として採用されやすい。
言語の技術力だけでなく、スキルの幅も、エンジニアになるのに必要なもの。
実務経験がなくても、本記事で上げた技術(もしくは業務や受託で使われている技術)を習得できれば、独学でも相当の開発力が身に付くでしょう。(僕はそう信じたい。)
参考
[ツール、エディタ]
【超初心者向け】ターミナルとは?Macの便利ツールを徹底解説
GitLab って何?
GitLabの運用方法を入門者向けに解説!GitHubとの違いも比較。ダウンロード、日本語化する方法も紹介!
GitHub.com・BitBucket.org・GitLab.comの月額料金比較 + α
GitLab自社運用のための注意点とノウハウ(2018/06版)「CI/CDツール」
iOS・Androidで、CI/CD環境を作る際の選択肢
iOS用のCIサービスBitriseを使ってみた
いまさらだけどCircleCIに入門したので分かりやすくまとめてみた
AndroidのCIサービスをCircleCIからBitriseへ移行しました
fastlaneを使ってみる
iOSアプリ申請を驚くほど簡単に!fastlaneではじめる自動化入門
SwiftLint × Sider + SwiftFormat で Swift らしくリファクタする
reviewdogによるGoのコードレビュー
Danger から Danger-Swift 移行への手引き「データベース」
Realmは「CoreDataもよくわかってないのに新しいDBなんて・・・」という人にこそオススメ
「Realmの基礎知識 〜特徴と強みの再認識〜」
RealmSwift vs CoreData
CloudKitについて調べてみた(その1)
iOS / iPadOS 13からCore DataとCloudKitの連携が自動化されたということで試してみた
mBaaSとしてのApple CloudKit の利用シーンと利用制限についての考察
Firestore導入前に知ってほしい。3層に分解して、メリット・デメリット比較と使いどころを考える「フレームワーク、API」
iOSアプリ個人開発で使ってるツールとかノウハウを公開してみる
モバイルアプリ開発ツールメモ2016
SwiftUIのすすめ – 1.メリットとデメリット –
Alamofire vs URLSession: a comparison for networking in Swift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)
RxSwiftを“チーム開発に”導入する話
SwiftのCombineを知る
【Server-side-Swift】【Vapor】iOSアプリエンジニアが挑戦する初めてのサーバーサイド【勉強会レポ】
iOSDCでVaporを布教してきた
Firebase Vs Vapor
[パッケージ管理ツール]
[Swift] CocoaPodsとCarthageの違い / ライブラリ管理
CarthageとCocoaPodsの違いを経験交えて比較する
Xcode 11 + Swift Package Managerでライブラリを管理する
Swift Package Manager vs CocoaPods vs Carthage for All Platforms
iOSアプリのライブラリ依存管理ツールとして Swift Package Managerを使うのは まだしばらく先かなと思った話「ユニットテスト、XCTest」
XCTest入門 (Swift) ~UITest編~
https://techblog.yahoo.co.jp/advent-calendar-2018/ios/「アーキテクチャ」
現場で選ばれているiOSアーキテクチャ
iOSアプリアーキテクチャ比較検討(Cocoa MVC,MVVM,MVP,CleanArchtecture)
MVCモデルについて
今更MVCとかでiOSアプリつくってみた(Swift)・改「サービス、他」
受託開発での iOS アプリプロジェクト新規作成プラクティス(上編:Xcode 編)
地方エンジニアのための、オススメアプリ&サービスまとめ【思考整理/情報収集】
いちばん詳しい Sketch / XD / Figma / Studio の比較 〜1. 導入と背景知識
いちばん詳しい Sketch / XD / Figma / Studio の比較 〜6. まとめ
9 Skills you Require to Get Hired As An iOS Developer
iOS Developer Skills Matrix
- 投稿日: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
を指定することができるので、色々と柔軟なレイアウトが作れそうですね。