- 投稿日:2020-03-15T23:30:29+09:00
UIToolbarをコードで実装する方法(iPhone8,iPhone11対応)
1.はじめに
アプリ作成のために、UIToolbarが必要になったのですが、ストーリーボードを使わない方法で作成した場合、
昔の記事はSafeAreaという概念が無かったiPhoneX以前のものが多く、実装に苦労したので備忘録として掲載します。
(もし間違いがございましたら、遠慮なく指摘していただくと幸いです)2.環境
Xcode Version 11.3.1
参考記事
Custom UIToolbar too close to the home indicator on iPhone X3. タブバーの高さを直打ちで入力した場合の問題
私が検索した限り、タブバーの高さは直打ちで決めていることが多かったように感じます。
例えば、こんな感じです。
コードを直打ちした場合、import UIKit class testVC: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBlue configureToolBar() } func configureToolBar() { // ツールバーの高さを直打ち let footerBarHeight: CGFloat = 100 // ツールバーのインスタンス化 let toolbar = UIToolbar(frame: CGRect( x: 0, y: self.view.bounds.size.height - footerBarHeight, width: self.view.bounds.size.width, height: footerBarHeight) ) toolbar.barStyle = .black //戻るボタンの実装 let backButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 40)) backButton.setTitle("Back", for: .normal) backButton.addTarget(self, action: #selector(back), for: .touchUpInside) let backButtonItem = UIBarButtonItem(customView: backButton) //ボタンを左右に分けるためのスペースの実装 let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) //進むボタンの実装 let nextButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100)) nextButton.setTitle("Next", for: .normal) nextButton.addTarget(self, action: #selector(goToNext), for: .touchUpInside) let nextButtonItem = UIBarButtonItem(customView: nextButton) // ツールバーにアイテムを追加する. toolbar.items = [backButtonItem,flexibleItem,nextButtonItem] self.view.addSubview(toolbar) } // 戻るボタンをクリックした時の処理 @objc func back() { print("戻るボタンがクリックされた") } // 進むボタンをクリックした時の処理 @objc func goToNext() { print("進むボタンがクリックされた") } }ここでの
let footerBarHeight: CGFloat = 100のように直打ちしてしまうと、下記の画像のように、
Appleのガイドラインには準拠していませんし、見た目もイマイチです。
4. SafeAreaLayoutGuideを使用した場合
この問題を解消するにあたり、SafeAreaLayoutGuideを使ったところ、下記の画像のようになりました。
NSLayoutConstraint.activate([ toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), toolbar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), toolbar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), toolbar.heightAnchor.constraint(equalToConstant: 50) ])なお、コードはこのようになります。
SafeAreaLayoutGuideを使用した場合import UIKit class SelectPostImageVC: UIViewController { let toolbar = UIToolbar () override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBlue configureToolbar() } func configureToolbar() { self.view.addSubview(toolbar) toolbar.barStyle = .black toolbar.translatesAutoresizingMaskIntoConstraints = false //戻るボタンの実装 let backButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100)) backButton.setTitle("Back", for: .normal) backButton.addTarget(self, action: #selector(back), for: .touchUpInside) let backButtonItem = UIBarButtonItem(customView: backButton) //ボタンを左右に分けるためのスペースの実装 let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) //進むボタンの実装 let nextButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100)) nextButton.setTitle("Next", for: .normal) nextButton.addTarget(self, action: #selector(goToNext), for: .touchUpInside) let nextButtonItem = UIBarButtonItem(customView: nextButton) // ツールバーにアイテムを追加する. toolbar.items = [backButtonItem,flexibleItem,nextButtonItem] self.view.addSubview(toolbar) //ここでSafeAreaLayoutGuideを使用 NSLayoutConstraint.activate([ toolbar.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor), toolbar.leadingAnchor.constraint(equalTo:view.safeAreaLayoutGuide.leadingAnchor), toolbar.trailingAnchor.constraint(equalTo:view.safeAreaLayoutGuide.trailingAnchor), toolbar.heightAnchor.constraint(equalToConstant: 50) ]) } // 戻るボタンをクリックした時の処理 @objc func back() { print("戻るボタンがクリックされた") } // 進むボタンをクリックした時の処理 @objc func goToNext() { print("進むボタンがクリックされた") } }
- 投稿日:2020-03-15T23:22:35+09:00
シワ(Sign in with Apple) 対応
Sign in with Appleとは
Apple ID を利用したサインイン方法。
サードパーティーログインを使用しているアプリは2020/04までにSign in with Appleに対応する様に義務化した。App Storeに提出されるすべての新しいAppやアップデートは、2020年4月30日までにこれらのガイドラインに従う必要があります。
引用元:https://developer.apple.com/jp/news/?id=03042020dそうです。いつものAppleの無茶振りです。
対応しないとリジェクトされてしまう為、仕方なく実装する事に、、
まずは仕様からアプリへの公開情報
アプリに公開される情報は大きく分けて以下の2つです。
- 名前
- メールアドレス
- 公開メールアドレス
- プライベートメールアドレス
これらの値はSign in with Apple新規登録時かユーザーがアプリと紐付け解除を行った後の初回のみ設定する事が出来ます。
名前
アプリに連携する名前はアプリ毎にユーザーが設定することができます。
デフォルトではiCloud アカウントに設定している姓名が入ってきますが、公開したくない場合は任意の名前に変更可能になっています。公開メールアドレス
ユーザーがiCloudの登録に使用しているメールアドレス。
* 実際のメールアドレスなので転送を停止する機能などは今回対象外プライベートメールアドレス
認証時にユーザーがメールアドレス(Apple ID)を非公開設定すると、Appleが自動生成したランダムな英数字のメールアドレスをアプリ(サービス)に連携する。その結果ユーザーは自身のメールアドレスを公開する事なくアプリでログインが可能になる。
* アプリとのメールやりとりはAppleが アプリ→自動生成メールアドレス→プライベートメール という形でメールを転送してくれる。(設定の必要あり)
* ユーザーはメールの転送を停止したり、転送先のメールアドレスを変更する事が出来る。このプライベートメールアドレスは出来るとユーザーにとって何が嬉しいのか?
サービス登録時に実際のメールアドレスを使用するとサービスの利用を停止した後にもメールアドレスを保持されて悪用されるリスクがある。そこでこのプライベートメールアドレスを使用すると実際のメールアドレスはサービスに公開せず、利用停止したい場合には紐付け解除(プライベートメールアドレスの破棄)を行えば悪用されなくなるというメリットがある。アプリとの紐付けの解除
iCloudの管理画面からアプリとの紐付け解除を行うことが出来る。
ユーザーにとって※iOS13以上では[設定>Apple ID(一番上の箇所)>パスワードとセキュリティ>Apple IDを使用中のApp]からでも解除出来る
CSRF対策
Sign in with AppleはCSRF対策の為にAppleの認証APIを実行する際にstateを指定することができる。
簡単に言うとなりすまし防止の為にアプリで送信時に作成したstateとAppleの認証時に返されたstateが一致しているかを確認する。完璧に仕様が分かったことで対応する事を以下に記載。
対応内容
あくまでアプリが対応する内容を記載する。
- コーディング
- iOS13以上の対応
- iOS12未満の対応(必須ではない)
- Android対応(必須ではない)
- Apple Developerでの設定
- Sign in with Appleの有効化
- メールのホワイトリスト追加
- ServiceIDの登録
- Xcodeの設定
- Sign in with Apple有効化
- デザイン対応
- ガイドラインに沿ったデザイン
iOS13での対応
Appleのログインボタンを生成する。
LoginViewController.swiftfunc setupProviderLoginView() { let authorizationButton = ASAuthorizationAppleIDButton() authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside) self.loginProviderStackView.addArrangedSubview(authorizationButton) }ユーザーの姓名とメールアドレスを受け取る様な認証のリクエストをAppleに対して行う。
LoginViewController.swift@objc func handleAuthorizationAppleIDButtonPress() { let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.presentationContextProvider = self authorizationController.performRequests() }Delegateで結果を受信する。
LoginViewController.swiftextension LoginViewController: ASAuthorizationControllerDelegate { func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { // 成功した場合 let userIdentifier = appleIDCredential.user let fullName = appleIDCredential.fullName let email = appleIDCredential.email } } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { // エラーが発生した場合 } }iOS12以下での対応
WebViewを利用してApple JSに対応する必要があります。
サーバサイドの実装が必要だったりAppleボタンを自作しなければならなかったりするので
出来ればiOS13以降のみの対応の方が無難だと思います。デザイン対応
Appleのガイドラインの記載を抜粋。
* Appleのボタンを目立つようにする
* 他サードパーティのログインボタンよりも小さくしないこと
* スクロールしないでもAppleのボタンが表示されるようにする
* ボタンの色は背景色と被らないようにする
などなどガイドラインに従わないとリジェクトされます。
実際に初めてSign in with Apple対応した時にはボタンと背景の色やフォントサイズなどでリジェクトされました。
(iOS12対応の為にボタンは自作しました)https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/
まとめ
iOS12以下に対応する為には、自作したボタンをAppleのデザインに合わせたり認証をWebViewで行ったりしないとならなく別対応が必要なので骨が折れますが、
iOS13のみなら基本Appleが提供しているSDKを使用すれば比較的簡単に開発が済みます。
最近(2020/03時点)リジェクトが厳しくなっている気がするのでデザインのガイドラインなどはしっかり読んで対応した方が良いです。参考URL
- 投稿日:2020-03-15T23:21:23+09:00
ライブラリ作ったから見てくれ
申し訳ありません。「見て下さい。」ですね。
調子乗りました。気を取り直して
こんにちはこんばんは。初投稿のいるべです。
昨年クリスマスに独り身でしにたくなったのでswiftの勉強を始めました。
情報系の学部にいながらプログラミングとは関わらない生活をしていたのでプログラミング歴は実質3ヶ月程となります。今、自分のお気に入りのものを教えてよって主旨のアプリを作成してます。SNSは素人には無理ゲーとか言う声が聞こえてきそうですがまぁやるだけタダなので黙ってて下さい。
今回は表題の通り。そのアプリを作る上で欲しくなった機能をライブラリとして作成したので、「まぁ見てって下さいよ」というところでつらつらと書かせて下さい。
ただの宣伝にならないように色々と書けたらいいなと思います。まず前提。なんでこんなものを作ったか、だ。
いやいや、その「こんなもの」を先に言えよと思う方もいらっしゃるかも知れんがスルーします。
UICollectionViewって意味分かんなくね?
玄人の皆様方に置かれましては何が?と思われるかも知れないが、
- まずdelegateが意味わからん。
- flowlayout何それ??
- そもそもRunさせても何も表示されん、、
その他壁は沢山あると言っても過言では無い。(断言)
少なからず全く調べずにcollectionViewを使って意図した挙動を実装することができる人はそう多く無いのではと考えている(え?みんなふつーに書けるの、、?)みんな悩んでるやろし作ったろ!
まぁもちろん既に上位互換ライブラリは存在していますが経験です。沢山勉強になりました。
僕と同じような初心者の皆様方も、「既にあるからそれ使おう」「既にあるから作るのやめよう」と言うのは悪くも無いけど良くも無いことがあるということを覚えておきましょう。たまには遠回りも必要ということです。お待たせたしました。
今回作ったものはこちらになります。
と画像をのっけてもインパクトがない。。
真ん中下のネコを妹に描いてもらった。ほんの1分程度でさらっと。すごい。タグを簡単に生成したいやんってことでCheckableTagというライブラリを作成しました。
簡単に説明しますと、簡単にタグを作成できるライブラリです。
更に、タップしたら色が変わるようになっているのでチェックボックスのような使い方もできます。
画像を見ての通り、タグの形にもいくつかあるので選ぶのに悩んじゃいますね?(☝︎ ՞ਊ ՞)☝︎導入
現在はPodのみの対応です。
pod 'CheckableTag'
を追加してpod installして下さい。使い方
import UIKit import CheckableTag class ViewController: UIViewController { let items = ["Hello"] let tagView: CheckableTag = { let view = CheckableTag() return view }() override func viewDidLoad() { super.viewDidLoad() tagView.frame = self.view.bounds self.view.addSubview(tagView) tagView.dataSource = self } } extension ViewController: CheckableTagDataSource { func getSelected(sender: CheckableTag) -> [Bool]? { return nil } func getItems(sender: CheckableTag) -> [String] { return items } }最低限のコードはこれだけです。
これで、下のような感じになります。*トリミングしてあります。
タップすると、
色が変わります。プロパティ
まぁここは興味があれば自由に弄ってみて下さい。
いるべ的には下記の設定が好きです。//tagの形を変更 tagView.cellType = .curve //tagの表示スタイルを変更 tagView.cellStyle = .groove //ユーザが選択可能かどうか指定 tagView.canSelected = false //フォントサイズの指定。fontsizeによってtagのサイズが変わる。 tagView.fontSize = 20色の変更
/// 選択状態の色を指定する /// - Parameters: /// - text: テキストカラー /// - back: バックグラウンドカラー /// - line: 枠線カラー tagView.setSelectedColor(text: .white, back: .red, line: .black) /// 非選択状態の色を指定する /// - Parameters: /// - text: テキストカラー /// - back: バックグラウンドカラー /// - line: 枠線カラー tagView.setUnSelectedColor(text: .gray, back: .white, line: .gray)選択状態の取得
//存在する全てのタグの選択状態を取得します。 //trueのとき選択されている //falseのとき非選択状態。 let isSelectedArray: [Bool] = tagView.getIsSelected()細かいことを指定する場合
DataSourceの使用例
tagView.dataSource = self ------------------------------ extension ViewController: CheckableTagDataSource { ///初期選択状態を指定できる。 ///canSelect = falseにしてこれと組み合わせればタグになる。 func getSelected(sender: CheckableTag) -> [Bool]? { return [true, false, true, false, true, true] } ///tagを生成するためにタグのテキストを伝えられる。 func getItems(sender: CheckableTag) -> [String] { return items } }Delegateの使用例
tagView.delegate = self ---------------------------- extension ViewController: CheckableTagDelegate { ///セルを選択したときの動作を設定できる。 func didSelected(cell: CheckableCellProtocol) { print(cell.textLabel.text) } }Animationの使用例
tagView.animation = self ---------------------------- ///このアニメーションは意味不明なのでただの参考として扱って下さい。 extension ViewController: TouchCellAnimationProtocol { ///タップし始めのときに行われるアニメーション。 func touchStartAnimation(cell: CheckableCellProtocol) { UIView.transition(with: cell, duration: 1.0, options: [.transitionFlipFromTop], animations: nil, completion: nil) } ///タップ終わりのときに行われるアニメーション。 func touchEndAnimation(cell: CheckableCellProtocol) { UIView.transition(with: cell, duration: 1.0, options: [.transitionFlipFromBottom], animations: nil, completion: nil) } }選択状態について
collectionViewやtableViewの選択状態を管理する時にスクロールしたら選択状態がずれていることがあります。
これはセルごとで選択状態を管理するのではなく、セル全ての選択状態を管理する配列を用意してそれを読んであげればそんな問題とはおさらばです。///collectionviewを使っている上位view //cellに代入するテキストの配列。 private(set) var items: [String] = [] //cellの選択状態を管理する配列 public lazy var isSelectedItems: [Bool] = { return [Bool].init(repeating: false, count: items.count) }()///cellが生成されるタイミング /// UICollectionViewDataSourceを継承 public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { //cellを再利用するために引っ張り出してくる let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) //選択状態を管理している配列から、今扱っているcellに該当する選択状態を見て、それに見合ったスタイルを反映する。 if isSelectedItems[index.row] { cell.selectedColor(text: textColor, back: backColor, line: lineColor) } else { cell.unSelectedColor(text: textColor, back: backColor, line: lineColor) } return cell }Enumは便利。
Enumってすごいですね。僕は気持ちが先走ってよくタイポをしてしまう人間なのですが,
予測変換で出てこればタイポもないし、後からの要素の追加も楽々!今回はEnumと後述するProtocolに救われました。public enum CellType: String { case square = "SquareCheckableCell" case curve = "CurveCheckableCell" case round = "RoundCheckableCell" case circle = "CircleCheckableCell" }これは実際のコードです。Stringがうだうだしてますね。
タイポしやすい文字列も
cellType.rawValue
を使えばここで設定している文字列が取得できますからタイポが一気に減ります。
最初は
case square
だけだったんですが、一度一通り作って仕舞えばもう簡単!
case *** と新しい要素をここに追加してビルドします(おい)
もちろんビルドは通りません。そのビルドに対して出たエラーのところを修正すればあら不思議。
もうまた別の要素が使えるようになっているではないですか。///指定されたcellのタイプを返す。 public func getCellType(collection: UICollectionView, index: IndexPath) -> CheckableCellProtocol { switch cellType { case .square: return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! SquareCheckableCell case .curve: return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! CurveCheckableCell case .round: return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! RoundCheckableCell case .circle: return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! CircleCheckableCell } }今回はご覧の通り似てるけど違うものをいくつも作ったのでstrategyパターンを採用しました。(実は後付け)
これとenumのおかげで今後も変更がだいぶ楽になりそうです。Protocolくんありがとう。
protocolの何が便利かというと後からの変更がとても楽です。追加も楽です。
protocolで決まりを作っておけば後はボーッとコーディングできる。
import Foundation import UIKit public protocol CheckableCellProtocol: UICollectionViewCell { ///cellの細かな表示を変更する var cellStyle: CellStyle { get set } ///cellとcontentViiewの間のマージン var margin: CGFloat { get } ///collectionviewに最初からあるview var contentView: UIView { get } ///textを表示するために一番中心に配置されるview var textLabel: UILabel { get } ///アニメーションを行うためのプロトコル var animationProtocol: TouchCellAnimationProtocol! { get set } ///labelに表示するテキストをセットする func setTextToTextLabel(textName: String) ///選択時の色設定 func selectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) ///非選択時の色設定 func unSelectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) ///cellを設定 func setCell() ///normalstyleの設定。cellにぴったりくっつく func setNormalStyle() ///groovestyleの設定。cellとの間に少し溝を作る func setGrooveStyle() } ///共通してるものはもう作っておく extension CheckableCellProtocol { public var margin: CGFloat { return LayoutConstants.margin } public func setTextToTextLabel(textName: String) { textLabel.text = textName } public func selectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) { textLabel.textColor = textColor?.selectedColor ?? .white contentView.backgroundColor = backColor?.selectedColor ?? .init(red: 255 / 255, green: 100 / 255, blue: 100 / 255, alpha: 1) self.layer.borderColor = lineColor?.selectedColor?.cgColor ?? UIColor.gray.cgColor } public func unSelectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) { textLabel.textColor = textColor?.unSelectedColor ?? .gray contentView.backgroundColor = backColor?.unSelectedColor ?? .init(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1) self.layer.borderColor = lineColor?.unSelectedColor?.cgColor ?? UIColor.gray.cgColor } public func setCell() { switch cellStyle { case .normal: setNormalStyle() case .groove: setGrooveStyle() } } public func setNormalStyle() { textLabel.frame = contentView.bounds } public func setGrooveStyle() { contentView.frame = CGRect(x: self.bounds.minX + margin, y: self.bounds.minY + margin, width: self.bounds.width - margin * 2, height: self.bounds.height - margin * 2) textLabel.frame = contentView.bounds } }protocolのextensionを使えば、必ず同じな部分の実装を楽に共通化できるのでとても便利。まぁclassを継承しても良いんだけどprotocolは直接インスタンス化できないところが好き。
ストラテジーパターン
ストラテジーパターンを使うと同じようなものをいくつも作成する時に追加が楽。変更も楽。
やり方は、ルール(プロトコル)を作ってそのルールに則って種類の分かれる奴らをそれぞれ作る。今回で言えばCheckableCellProtocolを作ってそれに則ったcellをSquare, Curve, Round, Circleと作った。それらを利用する側は外部からそのプロトコルに則ったインスタンスを貰うかそれを特定できる情報をもらうようにすれば完成。後は簡単に追加削除変更なんでもござれ。
具体的には上記のgetCellType関数が肝になっているのでgithubから見てみてね。さてこのライブラリの完成度は如何程か?
良かった点
- 割と使いやすいのでは?
- 変更しやすかった。楽しくコーディングできた。
- 自分の理想にかなり近い。
改善点
- はいまずUITestが通りません。意味がわかりません。調べたところdelegateが動作していないのが原因っぽいのですがだからと言ってどうすれば解決するのかさっぱりわからない。要勉強ですね。
- delegateでタグ同士の間のスペースとか指定できるようにしたい。
- github全部ニホンゴ。
- コードレイアウトマンなのでstoryboardで使えるか確認していない。。(近日中に必ず確認して使えるようにするつもりではあるが)
- CellTypeもCellStyleももっと種類増やしたい。
- タグ左寄せの設定もできるようにしたい。
自己評価は120点!良くがんばりました。これからも頑張りましょう!(文句言うな。)
初心者をはじめこの記事を読んで下さった皆様方。
軽い感じで読みやすくしようと思って書いていましたがそうでもなくなってしまいましたね。
もっと色々書きたかったけど難しいし体力切れや。
飛ばし飛ばしでもここまで読んで下さってありがとうございました。解説の部分はあんまり参考にならなかったと思います。定期的にアウトプットしてしっかり言語化することが大事だと感じました。
改善点はまだまだありますがぜひ一度使ってみて下さい。そしてissueやPR投げてくださると嬉しいです。なぜ「初心者をはじめ」と書いたかと言いますと、このコードは簡単なコードで書かれています。まぁ初心者が書いている+他人のコードなので読みにくいかも知れませんが。もっとプログラミング出来るようになりたいと考えている方は、OSSにPR投げたりしてcontributionを重ねていくことって大事だと思うのでそれのとっかかりとして、まずこのライブラリにPRを投げるのも良いと思うよってことが言いたかったです。
READMEのちょっとした修正やタイポ修正などでもこちらとしては本当に助かりますしいわゆるwin-winですね。(古い?)こんなのどうや?って気軽に投げて下さい。※この記事の内容は変なこと言っている可能性も大いにあります。情報の取捨選択をうまく行って、僕には文句ではなく指摘をしてくださると踊って喜びます。
長文駄文失礼しました。ありがとうございました。
- 投稿日:2020-03-15T21:19:57+09:00
signal SIGABRTの解決策(画面遷移時:segue)
前提として、signal SIGABRTとはシグナルが中断すること。バグです。
この原因はいくつかあるそうで、解決策は一つではありません。ですので、こういった記事をいくつも調べて一つずつ確かめるしかありません。僕もそうしました。。。少し骨が折れます。
今回はその中で僕がsignal SIGABRTをおこした原因と解決策を、一例として簡潔にまとめましたので参考にしていただければと思います。
結論から言いますと、僕の場合原因は二つありました。(だからどれか一つを改善してもうまくいかなくてやり直す、という抜け出せないループにハマってしまっていたのです。)
まず一つ目ですが、segue先のOutletLabelがダブっていて⚠️マークになっていたことです。スクリーンショットする前に訂正してしまったので画像では正常ですが、『Outlets』という欄のlabelがダブっていました。
segue先のLabelのエラーってなんか関係ないと思ってたんですけど、これの直後に解決できたので、関係あったんだと思います。。。(よくわかっていません)
二つ目ですが、segueボタンのidentifierを入力していなかったことです。
画像にあるように、segueのボタンを選択した状態で右上のattribute inspectorを選択して、identifierに遷移先のストーリーボード名を入力
以上のふたつを実行したら、解決できました。(汗)
皆さんがこの記事の方法で解決できるかはわかりませんが、原因が一つだとは限らないということを知っていただければ、僕のような負のループには陥らないで済むのではないかと思います。
- 投稿日:2020-03-15T21:08:17+09:00
swift コードで単色の画像を生成する方法
var image: UIImage? { let rect = CGRect(x: 0, y: 0, width: 1, height: 1) UIGraphicsBeginImageContext(rect.size) guard let context = UIGraphicsGetCurrentContext() else { return nil } context.setFillColor(UIColor(red: 1, green: 1, blue: 1, alpha: 0.8).cgColor) context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image }
- 投稿日:2020-03-15T19:12:31+09:00
エラーが起きた際に、BreakPointを自動配置してエラー箇所を見つけ出す。
例えば、Thread 1: signal SIGABRT等の抽象的なエラーコードが表示された場合、原因がピンポイントで提示されないのでどんな対策を打てばいいのかわからない時があります。
そんなときは、BreakPointを自動配置させて、原因となるコードをあぶり出しましょう。
僕も今回調べて知ったのですが、方法は簡単でした。
まず、ブレークポイントナビゲータを開きます。
そして、左下角にあるプラス『+』ボタンを押し、exception breakpointを押します。
これで完了!
こいつのおかげで、間違ったコードにぶち当たった時に、そこでbreakpointを勝手に打って止まってくれます。
そうすれば、そのコードについてのエラー記事を検索できたり、質問投稿しやすくなります。ありがたい。。
- 投稿日:2020-03-15T16:45:55+09:00
NAVITIMEのiOSアプリで使われているライブラリを調査!
NAVITIME
NAVITIME(ナビタイム)は、ナビタイムジャパン社が提供する、電車や車、徒歩などを使った経路を一度に検索できる総合ナビゲーションサービス。
引用先: https://ja.wikipedia.org/wiki/NAVITIME設定アプリからライブラリのライセンスを確認
AFNetworking
- ネットワーク通信のライブラリ
- Alamofireの次に人気のあるHTTP通信ライブラリ
AWS
- Amazon Cognito を使用すれば、ウェブアプリケーションおよびモバイルアプリに素早く簡単にユーザーのサインアップ/サインインおよびアクセスコントロールの機能を追加できます。Amazon Cognito は、数百万人のユーザーにスケールし、Facebook、Google、Amazon などのソーシャル ID プロバイダー、および SAML 2.0 によるエンタープライズ ID プロバイダーを使用したサインインをサポートします。 引用先: https://aws.amazon.com/jp/cognito
- AWSのCoreライブラリ
- 共通的な処理が入っているそうです。
- Amazon Kinesis でストリーミングデータをリアルタイムで収集、処理、分析することが簡単になるため、インサイトを適時に取得して新しい情報に迅速に対応できます。Amazon Kinesis は、アプリケーションの要件に最適なツールを柔軟に選択できるだけでなく、あらゆる規模のストリーミングデータをコスト効率良く処理するための主要機能を提供します。Amazon Kinesis をお使いになると、機械学習、分析、その他のアプリケーションに用いる動画、音声、アプリケーションログ、ウェブサイトのクリックストリーム、IoT テレメトリーデータをリアルタイムで取り込めます。Amazon Kinesis はデータを受信するとすぐに処理および分析を行うため、すべてのデータを収集するのを待たずに処理を開始して直ちに応答することが可能です。 引用先: https://aws.amazon.com/jp/kinesis/
Firebase
- Crashlytics、Fabric
- Firebaseのクラッシュ解析ツールです
- ほとんどのアプリに入っていることが多いです。
FrameAccessor
- Frameのめんどくさい処理を省略できるライブラリです。
beforeCGRect newFrame = view.frame; newFrame.origin.x = 15.; newFrame.size.width = 167.; view.frame = newFrame;afterview.x = 15.; view.width = 167.; instead ofGTMSessionFetcher
- HTTP通信の拡張ライブラリです。
- Googleが出しているライブラリですがStarが123と少なめ。
HMSegmentedControl
- UISegmentedControlの拡張ライブラリです。
adjust
リアルタイムに売上、イベント数、セッション数、インストール数、クリック数といった定量データに加え、コホート分析に基づいて、広告がどのような効果を可視化するためのサービスです。
LicensePlist
- 設定アプリでライセンスを一覧化してくれるライブラリです。
Quick
- iOSのテスト用フレームワークです。
- XCTestとよく比較されますね。
Protobuf
- Swift でプロトコルバッファを使うためのツールです。
- 参考: Swiftでプロトコルバッファを使う
Repro
- Reproは従来型の定量分析に加え、各種分析機能からユーザーをターゲティングして、プッシュ通知やメッセージを配信できる機能を提供するアプリとWebサイトの成長支援プラットフォームです。 引用先: https://docs.repro.io/ja/
SwiftyBeaver
- カラフルで柔軟な軽量ロギングツール。
- コンソール、ファイル、クラウドプラットフォームのサポートにより、開発とリリースに最適です。
- 参考: Swiftのログプラットフォーム、SwiftyBeaverのセットアップ方法
TinyConstraints
- Auto Layoutをめっちゃ書きやすくしたライブラリです。
- 個人的にも普段から使っています。
Toast
- 簡単にトースト通知を表示することができるライブラリです。
Arm Treasure Data
最後に
今回はNAVITIMEをライブラリを調査してみました!!ライブラリの言語をみると
Objective-C
で開発されたものが多いので、NAVITIMEもObjective-C
で開発されているのではないでしょうか。
次回はSwift
で開発されているアプリのライブラリ調査をしてみたいと思います。
ありがとうございました!!
- 投稿日:2020-03-15T16:42:17+09:00
ブラウザアプリ作り(1) とりあえず動く状態にする
前回記事 iOSアプリを作って公開してみたいと思う
Youtubeなどの動画を観ながら別の作業をしたいことも多い訳です。よくある場面としては実況動画を見ながら関連する情報を検索する、というのがあります。あるいはTwitterのTLを見ながらYoutubeの動画音楽を再生したいということもありますよね(YoutubeMusicに課金するのはなんか悔しい)。そこで、これらのニーズに応えられるブラウザアプリを作っていきたいと思います。今回進んだのは以下です。
・WKWebViewを使った2画面ブラウザ
・セーフエリアの設定
・動画のブラウザ内再生まず、ブラウザの実装です。
以下のサイトを参考にさせていただきました。
SwiftUIでWebViewを使うコードは上Qiita記事の円コピなので省略します。WebView.swiftを新しく作成して、ContentView.swiftでWebView(loadUrl:)で呼び出すんですね。非常にわかりやすいです。
次に、画面を分割してみます。
VStackでビューを2つ並べてみます。
いい感じです。
次に、高さを調整します。Youtubeには画面の3割を、残りを他に割きます。
参考:SwiftUIの肝となるGeometryReaderについて理解を深めるContentView.swiftstruct ContentView: View { var body: some View { GeometryReader {geometry in VStack{ WebView(loadUrl: "https://www.twitter.com") .frame(width: geometry.size.width, height: geometry.size.height * 7 / 10) WebView(loadUrl: "https://www.youtube.com") } } } }下のセーフエリアがもったいないので、ここも使えるようにします。VStackに
.edgesIgnoringSafeArea(.bottom)をつけてやります。
最後に、デフォルトだとYoutubeを再生し始めると全画面になってしまうのですが、これが起きないようにします。
参考:How can I add web view preferences to a swift ui web view
変更するのは先ほど作成したWebView.swiftです。
WebView.swiftimport SwiftUI import WebKit struct WebView: UIViewRepresentable { var loadUrl:String var allowsInlineMediaPlayback: Bool{true} func makeUIView(context: Context) -> WKWebView { let webConfiguration = WKWebViewConfiguration() webConfiguration.allowsInlineMediaPlayback = true return WKWebView(frame: .zero, configuration: webConfiguration) } func updateUIView(_ uiView: WKWebView, context: Context) { uiView.load(URLRequest(url: URL(string: loadUrl)!)) } }これで、ひとまず使える当初の目的にかなったアプリにはなりました。
まだまだ機能が足りていないので、随時勉強&追加していきます。今回作成したコード
https://github.com/KanikaniYou/MultiTube
- 投稿日:2020-03-15T13:04:27+09:00
iPhoneで取得可能な磁気の種類
Core Motion
CMMotionManagers > CMMagnetometerクラス > magneticFieldプロパティ
地球の磁場 + 周囲の磁場 + デバイスのバイアス
- 磁力計からの生のデータを読み取る
- デバイス自体で生成される磁場の影響を大きく受ける
- 希土類磁石を近づけない限り、何にも反応しない。デバイス内の磁場は、局所的な外部の磁場や地球の磁場よりもはるかに重視される。
CMDeviceMotion > CMCalibratedMagneticFieldプロパティ
地球の磁場 + 周囲の磁場 - デバイスバイアス
- デバイスバイアス(オンボード磁場)に対して補正された磁力計の読み取り値
- iPhone内部の磁場の影響の除去
CoreLocation
CLLocationManager > CLHeading
地球の磁場 - 周囲の磁場 - デバイスバイアス
- フィルタリングしてローカルの外部磁場を除去
- 多くの種類の電子機器に見られる固定磁石から発生する磁場など、局所的な磁場の影響を受ける可能性がある。
- 局所的な外部磁場というのに、建物の鉄骨による影響の除去も含まれる。
- 他のセンサーからのデータを使用して、磁力計データを安定させてる。
- 磁場 x、y、zの生のデータは得られない。
参考
- 投稿日:2020-03-15T08:55:51+09:00
Swiftでfor文の最後だけ処理をする
for (i, message) in messages.enumerated() { // 全部にしたい処理 if (i == messages.endIndex - 1) { // 最後だけしたい処理 } }まず
enumerated()
を使うことでi(ここでindexでなく敢えてiを使ったのはenumerated()
で取得できるのはただのカウントだから)を取得できるが、逆にendIndexは配列しか持っていない。Swift3.0からfor文が使えなくなり
for-in
文かforEach
、もしくはmap
などしか使えなくなった。なので上記のenumurated()を使った「何回目の処理か」を取得する方法が散見されるが、これはindexでないというのが正しい説明のよう。https://qiita.com/motokiee/items/4886b603631a7c076f46
https://qiita.com/a-beco/items/0fcfa69cca20a0ba601c
messages.count
でも同じ値が帰ってくるはずだが、あれ、-1であってるんだっけ…。
guard
を使わなかったのは、処理を抜ける時にbreak
とreturn
の違いもあってこんがらがりそうだったから。break
は今の繰り返し処理を抜けて次の繰り返しを始めるが、return
を使っちゃうとfor-in
の処理がそこですべて終了しちゃう。
- 投稿日:2020-03-15T03:58:15+09:00
[Swift] 同一ドメインでのユニバーサルリンク(Universal Links)は動作しないので注意メモ
Web側とiOS側でユニバーサルリンクを設定したが何故か動作せず、調べたら同一ドメイン上では機能してくれない様でした。。
実装前の調査不足ということで、備忘録的なメモになります。概要
- 同一ドメインではユニバーサルリンクは機能しない(アプリが起動しない)
- サブドメインを使った代替策
同一ドメイン上ではアプリへ遷移ができない
ユニバーサルリンクは同一ドメイン上では動作しないため注意が必要です。
公式ドキュメントでも注意点として記載されていました。。
すでにWebで利用しているユーザーに対して急にアプリ起動したらUX的によろしくないのでAppleさんが制御しているらしいです。
(翻訳した自分の解釈が間違っていなければ)Support Universal Links:
https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.htmlサブドメインをリンク先に設定することで、ユニバーサルリンクを有効にすることができます。
とはいえ、サブドメインで分けるほどのページではないがユニバーサルリンクを有効にしたい場面があるかと思います。
次では、見た目上は同一ドメインでユニバーサルリンクが動作する例を紹介します。代替策
同一ドメインではユニバーサルリンクは無効ですが、サブドメインなどに遷移先を設定すれば有効になります。
以下例では、見た目上は同一ドメインでの遷移ですがユニバーサルリンクをサブドメインとしている代替策になります。# shop一覧画面(www.hoge.com)から詳細画面を開く場面を想定しています。 # 詳細画面のリンク先をサブドメイン(shop.hoge.com/shop_id)として設定。 # リンク先のサブドメインはリダイレクト先として詳細画面(www.hoge.com/shop/shop_id)を設定。 # 設定したリンクはユニバーサルリンクとして設定しているため、アプリが起動します。 # アプリ未インストール&Webでの表示では、リダイレクト先の画面(www.hoge.com/shop/shop_id)が表示されます。まとめ
- ユニバーサルリンクは同一ドメインでは機能しない
- 機能させるためには、サブドメインなど活用する(場面に応じてリダイレクト先を設定するなど対応する)
- ユニバーサルリンクを使用しない方法(URLスキーム)で実装できないかを考える
記載した情報に誤り等ございましたら、ご指摘をいただけると幸いですmm
- 投稿日:2020-03-15T02:57:52+09:00
Swift UI で SearchBar を使う
Search Bar をつけたい
SwiftUI でUISearchBarをつけようとすると、存外に苦労しました。おそらくまだ十分にAPIが開放されていないためだと思います。こういう場合、
UIViewControllerRepresentable
などを使い、SwiftUIで使えるようにしていくしかないのかなぁと思います。やりたいのは以下の通り、UIKit使ってたら普通にできたよね・・・?
tl;dr
- SearchBarを使いたい時はUINavigationControllerをUIViewControllerRepresentableでラップする。その際にUISearchControllerをセット
- NavigationBarしか使用しない場合は、親ViewのSafeAreaを
edgesIgnoringSafeArea(.vertical)
で調整- TabBarをでNavigationBarを囲っている場合は、親ViewのSafeAreaを
edgesIgnoringSafeArea(.top)
で調整後、NavigationBarが実装されているViewのSafeAreaをedgesIgnoringSafeArea(.vertical)
で調整する1. SearchBarをSwiftUIで使用する
- まずUINavigationViewControllerにUISearchControllerを追加して、それををSwiftUIで使用できるようにします。
struct NavigationViewControllerRepresentation: UIViewControllerRepresentable { typealias UIViewControllerType = UINavigationController func makeUIViewController(context: Context) -> UINavigationController { let viewController = UIHostingController(rootView: List{ Text("one").frame(height: 120.0) ... Text("six").frame(height: 120.0) }.navigationBarTitle("Title")) let navigationController = UINavigationController(rootViewController: viewController) navigationController.navigationBar.prefersLargeTitles = true viewController.navigationItem.searchController = UISearchController() return navigationController } func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { } }
- SwiftUI Canvasに表示させる
MyNavigationView.swiftstruct MyNavigationView: View { var body: some View { NavigationViewControllerRepresentation() } } struct MyNavigationView_Previews: PreviewProvider { static var previews: some View { MyNavigationView() .environment(\.colorScheme, .dark) // わかりやすくするためにダークモードで表示 } }2. SafeAreaを調整する
この状態だと、赤枠で囲ったように上下に隙間ができます。SafeAreaですね。
MyNavigationView.edgesIgnoringSafeArea(.vertical)
としたいところですがここではその衝動をぐっと抑えます。SceneDelegate.swiftclass SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { let contentView = MyNavigationView().edgesIgnoringSafeArea(.vertical) // [1] if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } }コメントの[1]にあるように、最下層のビューに対して
edgesIgnoringSafeArea(.vertical)
を適用してあげます。そうするとプレビュー上は隙間がありますが、実機で動かすと確かに上下の隙間がなくなります。
- 確認結果
右側のスクリーンショット、ステータスバーのところまで暗めの灰色になってるのが見えるでしょうか。
3. TabBarに対応させる
TabBarが絡んでくるとちょっとまた調整が必要になります。試しにそのままTabBarで囲い込んでみます。
MyNavigationView.swiftstruct MyNavigationView: View { var body: some View { /*NavigationViewControllerRepresentation()*/ TabView { NavigationViewControllerRepresentation().tabItem { Image(systemName: "1.circle") Text("tab1").tag(0) } Text("tab2").tabItem { Image(systemName: "2.circle") Text("tab2").tag(1) } } } }
![]()
ちょっと上下にTabBarとNavigationBarが食い込んでますね。これはどういわけか.top
で直ります。SceneDelegate.swiftclass SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { // let contentView = MyNavigationView().edgesIgnoringSafeArea(.vertical) let contentView = MyNavigationView().edgesIgnoringSafeArea(.top) if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } }
![]()
...やった、、、のか?と思いましたが右側のスクリーンショットを見てください。ステータスバーのところが暗めの灰色ではなく、黒色になっています。そこで、また調整をします。MyNavigationView.swiftstruct MyNavigationView: View { var body: some View { /*NavigationViewControllerRepresentation()*/ TabView { NavigationViewControllerRepresentation().tabItem { Image(systemName: "1.circle") Text("tab1").tag(0) } .edgesIgnoringSafeArea(.vertical) // 追加 Text("tab2").tabItem { Image(systemName: "2.circle") Text("tab2").tag(1) } } } }やりましたね。