- 投稿日:2019-06-07T23:29:15+09:00
Xcode 製品版でprintを出力しない例
func print(_ object: Any) { #if DEBUG Swift.print(object) #endif }
- 投稿日:2019-06-07T22:58:04+09:00
UIView, UIScrollView, UITableView の画像キャプチャのシンプルな実装
本記事の趣旨
下記の目的に対して、巧妙な実装をしている解説記事が多かったので、よりシンプルな実装を提案するものです。
- 任意の
View
のキャプチャ画像をUIImage
として取得する- 任意の
UIScrollView
/UITableView
のコンテンツ全体の画像をUIImage
として取得する万が一うまくいかなかったらコメントで教えていただけると幸いです。
方法
1. 任意の
View
のキャプチャ画像をUIImage
として取得するここでは、
UIView
の extension として記述します。UIView+getImage.swiftimport UIKit extension UIView { func getImage() -> UIImage? { // 確実に不透明なら、第2引数は true がよい UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0) guard let context = UIGraphicsGetCurrentContext() else { return nil } self.layer.render(in: context) let capturedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return capturedImage } }2. 任意の UIScrollView / UITableView のコンテンツ全体の画像を UIImage として取得する
キャプチャの瞬間だけ、
UIScrollView
/UITableView
のframe.size
をcontentSize
に変更してしまう方針です。
UItableView
はUIScrollView
を継承しているので、UIScrollView
の extension として記述します。
(先ほどのgetImage()
とconflict しないように別名のメソッド (getContentImage()
) とします。)UIScrollView+getContentImage.swiftimport UIKit extension UIScrollView { func getContentImage() -> UIImage? { UIGraphicsBeginImageContextWithOptions(captureSize, false, 0.0) guard let context = UIGraphicsGetCurrentContext() else { return nil } // 元の frame.size を記憶 let originalSize = self.frame.size // frame.size を一時的に変更 self.frame.size = self.contentSize self.layer.render(in: context) // 元に戻す self.frame.size = originalSize let capturedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext(); return capturedImage } }
- 投稿日:2019-06-07T22:47:16+09:00
Swiftの言語について
初めまして!きゅうすけと申します!
iOSエンジニアの私が、自社の定例イベント 「TokyoUppersBoost」 で学んだ基礎の部分をまとめたものです!
メモ感覚ではありますが、これ違うぞってことがあったらぜひ教えてくださいー!(● ˃̶͈̀ロ˂̶͈́)੭ꠥ⁾⁾変数と定数
変数とは
再代入が必要な場合に使う
varvar name = "田中"定数とは
再代入が不要な場合に使う
letlet name = "田中"値の代入方法
定数は後から値の変更ができないから、代入しようとするとエラーになる
var name = "田中" name = "佐藤" let name = "田中" name = "佐藤"//エラー型
値の種類のこと。
Swiftにおいては変数や定数は、 必ずどれかの型に属していなければならないルールがある。
そのため、変数や定数を宣言するときは型の種類を指定する必要がある。var drink: String = "コーヒー"型推論
swiftの場合は文脈から読み取ってくれる → わざわざ明示的に型を書かなくてもいい!
// 型推論 let name = "田中" //String型 let number = 10 //Int型 let Name = 15.5 //Double型基本的な型
String:文字列
Int:整数
Float:浮動小数点(32bit長)
Double:動小数点(64bit長)
Bool:真偽値
Array:配列
Dictionary:辞書型String型
文字列を表す型 ""で囲う
数値を入れようとするとエラーが出るlet name: String = "佐藤"Int型
整数を表す型
let age: Int = 22Double型 / Float型
小数点がある値を宣言する型。不動小数点型とも言われる。
Double型:大きい値(64bit)
Float型:小さい値(32bit)Float型にしたい時はFloatと書く必要がある
let length = 125.55 //Double型 let width: Float = 100.55 // Float型Bool型
true、falseを表す型。
Bool型で宣言している場合は、trueとfalseしか代入できないlet flag = true //Bool型Array型
配列。複数の値が格納された型。
値を,で区切り、[]で囲む。
文字列の場合はString型で定義する。
0からカウントする。let color: String = ["赤", "青", "黄色", "緑"] let number: Int = [1, 2, 3, 4]Dictionary型
name:キーと値を1つのまとまりとして複数のまとまりを1つの値にまとめる型。
let person = ["name": "田中", "age": 22]Optional型と非Optional型
Swiftの非Optional型はnilを許容しない。
nil(いれられるものが何もない状態)を許さないからエラーが出るhello = "こんにちは" hello = nil //errorOptional型
nilを代入することができる
型の最後に? または!をつける。var name = String? var age = Int!? と ! の入れ方の違い
? → 値が入るかもしれないし入らないかもしれない
! → nilを入れることができない 暗黙的アンラップアンラップについてはまだ理解できていない…(´・ω・`)
非Optional型
型の後に何も付けない。var name = String var age = Int終わりに
ラップ、アンラップについても説明があったのですが、自分がまだちゃんと理解できていないため
もう少し教えてもらったり調べてから別で記事書こうと思ってます!!私が参加している勉強会 「TokyoUppersBoost」への参加者さん大募集中です!!
一緒にiOSについて学びましょう・:*+.(( °ω° ))/.:+↓私が作ったLPなのでよかったら見てください!٩( 'ω' )و
TokyoUppersBoostとは?
- 投稿日:2019-06-07T20:05:08+09:00
WKWebViewでローカルのindex.htmlを読み込んでscriptを実行してみた
ネットを介してのWebView表示ではなく、手元にあるHTMLをWebViewで表示、scriptタグ内のJSメソッドの呼び出し方法をまとめます。
SwiftでWebViewを表示させる場合、WKWebViewを使うことがほとんどだと思います。現在ではUIWebViewは非推奨です。そちらについては以下の記事がとても参考になります。
[参考元]:UIWebViewを使わない理由とWKWebViewを使う理由環境
OS: Mojave 10.14.5
Xcode: 10.1
Swift 4.2HTML
今回はindex.htmlをWKWebViewでロードし、
<script>
タグ内にあるJSメソッドを呼び出すことをします。
HTMLファイルはプロジェクト内に置きます。置き場は自由で構わないのですが、例として以下のように配置しておきます。例project ├─Resource │ ├index.html │ └sample.json │ └─View └SomeViewControllerHTMLのロード
ローカルのHTMLファイルはパスを取得する必要があり、取得には
Bundle.main.path
を使用します。
仕様ではBundle.main.path
はプロジェクトのBuild Setting
>Copy Bundle Resources
にあるファイルを取得するようです。
まずこの項目を確認し、取得したいHTMLファイルがなければ追加しておきましょう。[参考元]:Swiftでプロジェクト内のファイルを取得する際の注意点2つ
HTMLファイルのパスをURL型にキャストしてWebViewの
loadFileURL
の引数に与えます。これでWebViewにロードできました。func loadLocalHTML() { guard let path: String = Bundle.main.path(forResource: "index", ofType: "html") else { return } let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false) webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl) }Swift側からscript内のjsを実行
実行タイミング
<script>
タグにあるメソッドは、HTML全てがロードし終わってから呼び出さないとメソッド呼び出しに失敗します。
ロードが終わったことを通知するdelegateがWKNavigationDelegate
にあります。delegateの通知先は自分であることの宣言を忘れずに。
SomeViewContollerwebView.navigationDelegate = selfSomeViewControllerextension SomeViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { // ここでJSの実行をする } }JSの実行
evaluateJavaScript
を使います。引数のjavaScriptString
に、実行するメソッドをString
で渡します。
また、ここのString
ではjsの文法で書く必要があります。jsの知見が乏しい筆者はここで躓きました。[公式リファレンス]:evaluateJavaScript(_:completionHandler:)
<script>
の中身は以下のようなものと仮定して、initHTML関数に引数を渡し実行する方法を解説します。<script> function initHTML(id, jsonData, onload) { console.log('initHTMLだお'); console.log(id, jsonData); onload(); } </script>引数
Swiftは静的型付け、JSは動的型付けなので、ゆるい方向への値渡しです。
initHTMLの引数(id, jsonData, onload)は、Swift側ではそれぞれInt,Json,関数として渡します。基本は文字列リテラル内での値の展開
\()
を使います。
Jsonに関しては、Jsonファイルを読み取ってData
->String
に変換して、値展開で渡しています。Stringで渡してもWebView側でJsonとして扱ってくれます。
関数はjsの書き方であるアロー関数を用いて書きましょう。extension SomeViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { let intValue = 10 guard let json = getLocalJson() else { return } guard let strJson = String(data: json, encoding: .utf8) else { return } let script = "initHTML(\(intValue), \(strJson), () => { console.log('Swiftから渡された値だお') });" webView.evaluateJavaScript(script) { object, error in print(error ?? "成功") } } func getLocalJson() -> Data? { guard let path = Bundle.main.path(forResource: "sample", ofType: "json") else { return nil } let url = URL(fileURLWithPath: path) let json: Data? do { json = try Data(contentsOf: url) } catch { json = nil } return json } }コンソールの確認
JSの実行ができたらコンソールを確認しましょう。
しかしWebViewで実行したJSコンソールログはXcodeでは見れません。
ブラウザであるSafari
で見ることができます。開発
->端末名
->index.js
と選択するとWebインスペクターが開きます。呼び出しがうまくいってログが出力されています!
表示がない場合は左上らへんにある更新マークを押しましょう。次回
WebViewのJS内でprojectのソース(画像とか)にアクセスしたときに発生するエラー、
Orijin null is not byAccess-Control-Allow-Origin
編に続く...WWDC19あとがき
SwiftUIめっちゃすごい。iOSのUI回りに激震が走りましたね。
ここまでUIの作成ハードルが下がると我々iOSエンジニアの価値が相対的に下がっていくのでは…
Combine frameworkの登場により、Rxライブラリは今後必要なくなるのかな。ただObserverパターンの考え方は必要なのでここはそれなりにハードルは依然高そう。参考
- 投稿日:2019-06-07T18:58:56+09:00
Swift UI を使って簡単なSNS風(見た目だけ)を作りました。
SwiftUI を用いSNS風(見た目)を作ってみました
- 現状のswiftUIの導入方法についてはこちらをご覧ください。
- できたもの
- サンプルコード
- 縦スクロール
- 画像のハメかた
- 横スクロール
感想
今までより簡単にコードが書けるので学習コストもそんなにかからないと思いました!
flutter を勉強しようと思っているのですがどちらから勉強すればいいか迷っています。。。できたもの
サンプルコード
縦スクロール
まずはTableViewのような縦にスクロールできるものを作っていきます。
ContentView.swiftimport SwiftUI struct ContentView: View { var body: some View { NavigationView{ List{ Text("Row_1") Text("Row_2") } }.navigationBarTitle(Text("Groups").color(.green)) } }こんな感じになるはずです(なぜかNavigationBarが表示されません。)
また以下のように書くと
文字がいじれます。ContentView.swiftstruct ContentView: View { var body: some View { NavigationView{ List{ Text("Row_1") .font(.headline) .color(.red) Text("Row_2") .font(.footnote) } }.navigationBarTitle(Text("Groups").color(.green)) } }画像のハメかた
長くなりそうなので別の記事に書こうと思います。
- 投稿日:2019-06-07T18:34:39+09:00
iOS13でpresentViewControllerでの遷移はひとくせあり
iOS13でpresentViewControllerでの遷移はひとくせあり
iOS13のベータ版が公開されたので、早速実機にインストールして、
開発中のアプリをXCode11ベータ版でビルドして動作確認してみました。そこで、画面遷移の仕様が変わっていました。
具体的にはpresentViewController(セグエも同様でしょう)を使用したモーダルタイプの遷移後に、
前の画面が下に存在することが分かるようなレイヤーチラ見せ方式になっています。
※ナビゲーションのpushの遷移は従来と同様でした。連絡先アプリの例
iOS13 iOS12 これにより、iOS13ではキャンセルボタンを押さずとも
下方向にスワイプすると、今のビューを終了して前の画面に戻ることが可能になりました。iOS12でも同様の仕様の部分はありました。
例えばミュージックアプリの以下画面など下スワイプで前の画面に戻れる点では、すごく便利になりますね。
その反面、presentViewControllerで遷移後、
戻ることを想定しない構成で作られていたアプリは、
2019年秋以降のアップデート時には修正が必要になりそうです。その他にも、仕様の変更点があるようでしたのでそれは別途で。。。
・セグメントなどパーツの見た目の変更
・ダークモードによる背景色・テキストカラーへの影響など
- 投稿日:2019-06-07T18:31:28+09:00
TableViewのCell入れ替えでViewが透過されてしまったあなたへ
まくら
iOSアプリといえばUITableViewでございます。たぶん。
今回はそんなUITableViewの小ネタをひとつ。UITableViewは割と簡単な設定でセルの入れ替えを実現させてくれます。
extension TableWorkViewController: UITableViewDataSource { // ...省略... // 移動可能 func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { return true } // 移動実行 func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { print("source: \(sourceIndexPath)") print("dest: \(destinationIndexPath)") } }こんな感じで設定してやって、どこかで
self.tableView.isEditing = trueこんな感じで入れ替えアイコンが表示されるようになります。
基本的なものでいいのならこれだけ。簡単。では実際に入れ替えてみましょう。
お気づきだろうか?
セルの上に
UIView
を乗せているのですが、見えなくなっていますね。
どうやら入れ替えの時はセルの上に乗っているUIView
は透明にされてしまっているようです。何やらちょっと気持ち悪いので、対策を考えてみましょう。
ほんだい
という訳で本題。
自分も地味にハマってしまったのですが、ナイスな力技で解決している方がいたので、マネさせて頂きました。
参考:
https://medium.com/@vhart/tableview-cell-reordering-keep-the-color-4c03623d2399最高!この力技感!
元ネタの方を見て頂けばわかる通り、移動の時に
UIView
のbackgroundColor
に透明がセットされてしまうので、強制的に色を保持してやろうという訳です。そんなわけで作ったものがこちら
/// セルの入れ替え時に色をのキープしておくためのView class TableWorkColorKeepView: UIView { /// キープ用のbackgroundColor @IBInspectable var keepBackgroundColor: UIColor = .clear { didSet { self.backgroundColor = self.keepBackgroundColor } } /// 背景色 override var backgroundColor: UIColor? { set { // キープしている色と異なればリターン guard newValue == keepBackgroundColor else { return } super.backgroundColor = newValue } get { return super.backgroundColor } } }これを使うと、
こうなります。
キープ用の色を用意して、背景色が変更される時にその色と異なったら無視してやっているだけですね。
背景色をセットしたい場合はkeepBackgroundColor
の方に設定してやる感じです。
(Storyboardで初期値設定したかったので@IBInspectable
してやっています)しまいに
力技なので用法用量にご注意を。
もっとスマートなやり方あれば教えてつかあさい。
- 投稿日:2019-06-07T16:18:37+09:00
一部の画面だけSwiftUIを使いたいとき
本記事は公開済みのドキュメントを元に作成しています。今後仕様変更などにより記載内容と異なる場合があります。予めご了承ください。
はじめに
SwiftUIを触っていて、
「SwiftUIのView
とUIKitのUIView
間の連携ってどうやるんだ?」となったので調べました。
結論から言うと、UIHostingController
/UIViewRepresentable
でなんとかなりそうです。UIViewの中で画面の一部として使う
UIView/UIViewController
の中でSwiftUIのView
を使う場合は、UIHostingController
でラップしてあげる形になります。
こんな感じで。import SwiftUI class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let vc: UIHostingController = UIHostingController(rootView: SubView()) self.addChild(vc) self.view.addSubview(vc.view) vc.didMove(toParent: self) vc.view.translatesAutoresizingMaskIntoConstraints = false vc.view.heightAnchor.constraint(equalToConstant: 320).isActive = true vc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 16).isActive = true vc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -16).isActive = true vc.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true } } struct SubView : View { var body: some View { Text("にゃーん?") } }画面遷移先として使う
UIHostingController
はUIViewController
を継承しているので、そのまま遷移先としても使えます。import SwiftUI class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let button: UIButton = UIButton(type: .system) button.setTitle("?", for: .normal) button.addTarget(self, action: #selector(didTapTransitButton(_:)), for: .touchUpInside) self.view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true } @objc func didTapTransitButton(_ sender: UIButton) { self.present(UIHostingController(rootView: DetailView()), animated: true) } } struct DetailView : View { var body: some View { Text("にゃーん?") } }Viewの中でUIViewを使う
逆に
View
の中でUIView
を使いたい時は、UIViewRepresentableを継承して使います。
チュートリアルのCreating and Combining Viewsにも記載があります。import SwiftUI struct ContentView: View { var body: some View { ZStack { BackgroundView() Text("にゃーん?") } } } struct BackgroundView: UIViewRepresentable { func makeUIView(context: UIViewRepresentableContext<BackgroundView>) -> UIView { let view: UIView = UIView() view.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) return view } func updateUIView(_ uiView: UIView, context: Context) { // } }参考
UIHostingController - SwiftUI | Apple Developer Documentation
UIViewRepresentable - SwiftUI | Apple Developer Documentation
Creating and Combining Views — SwiftUI Tutorials | Apple Developer Documentation
- 投稿日:2019-06-07T14:16:37+09:00
無限ループするUIPageViewControllerとタブ
はじめに
ライブラリ「XLPagerTabStrip」を使って横スワイプでタブ・ページ切り替えしていた画面を
無限スクロールさせることになりました。有名なライブラリだしきっと簡単に無限スクロールできるよね。
と思い調査してみたら、意外とループ機能が無かった...ので自作しました。自分用の備忘録として、実装のポイントをメモします。
デモ
いろんなアプリでよく見かけますね
ページが切り替わるタイミングは3種類。
- ページを左右スワイプしたとき
- タブを左右スワイプしたとき
- 未選択のタブを押下したとき
タブは無限スクロールし、またページの切り替えに合わせて追従します。
選択されたタブは色が変わり、タブのタイトル幅に合わせてバーのサイズも変わります。環境
- XCode Version 10.1
- Swift 4.2
画面構成
タブはCollectionView、ページはPageViewController
タブ下のBarはUIViewで構成しました。
CollectionViewにはUILabelが乗ったカスタムCellをセットします。
ループ処理
実装するにあたってこちらのサイトにはとてもお世話になりました。
UIPageViewControllerをつかって無限スクロールできるタブUIを実装してOSSとして公開しましたUIPageViewControllerのループ処理
今回のサンプルコードでは画面をA,B,C,Dの4種類用意しました。
ページのループ処理は以下の通り。PageViewController.swiftpageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController)前後のVCを読み込むDelegateでメソッド「nextViewController」を呼び
PageViewController.swift// 画面A,B,C,DのUIViewControllerが格納された配列 private (set) var pageControllergrop = [UIViewController]() private func nextViewController(viewController: UIViewController, isAfter: Bool) -> UIViewController? { guard var index = self.pageControllergrop.index(of: viewController) else { return nil } index = isAfter ? (index + 1) : (index - 1) if index < 0 { index = self.pageControllergrop.count - 1 } else if index == self.pageControllergrop.count { index = 0 } if index >= 0 && index < self.pageControllergrop.count { return self.pageControllergrop[index] } return nil }前のVCが無ければ末尾のVCを、後のVCが無ければ頭のVCを表示させることで、ページをループさせます。
UICollectionViewのループ処理
高速スクロールしてもCellが見切れないよう、Cell数を項目の5倍用意しました。PageViewController.swiftfunc scrollViewDidScroll(_ scrollView: UIScrollView) { let isScrollCollectionView: Bool = (scrollView.className == "UICollectionView") ... // CollectionView スクロール時 if isScrollCollectionView { // CollectionView 1項目分の横幅 let listWidth = self.collectionView.contentSize.width / 5.0 // スクロールが左側のしきい値を超えたとき、中央に戻す if (self.collectionView.contentOffset.x <= self.cellWidth) { self.collectionView.contentOffset.x = (listWidth * 2) + self.cellWidth // スクロールが右側のしきい値を超えたとき、中央に戻す } else if (self.collectionView.contentOffset.x) >= (listWidth * 3) + cellWidth { self.collectionView.contentOffset.x = listWidth + self.cellWidth } } ... }画面左側に1ループ目/1番目のCellが来たら、3ループ目/1番目のCellへ
画面左側に4ループ目/1番目のCellが来たら、2ループ目/1番目のCellへ
強制的にスクロール位置を戻すことでループを実現させます。ページ切り替えに合わせてタブを追従させる
他アプリでは、ページ切り替え後にタブを移動させるアニメーションを割と見かけましたが
指の動きに合わせて動いてくれた方が見た目タイムラグが無さそうだな
と思ったので実装してみました。PageViewController.swift// スクロール開始位置 private var startPointPageX:CGFloat = 0 func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { // スワイプ前のスクロール位置を保持 self.startPointPageX = scrollView.contentOffset.x // 表示中のページにCellの中心値を合わせるため、CollectionViewの初回スクロール位置を設定 self.startPointCollectionX = { let listWidth = self.collectionView.contentSize.width / CGFloat(self.pagelistsCount) let contentOffsetX = listWidth * 2 + (self.cellWidth * CGFloat(self.selectedPageNum)) let centerMargin = (App.windowWidth - self.cellWidth) / 2 return contentOffsetX - centerMargin }() ... } func scrollViewDidScroll(_ scrollView: UIScrollView) { let isScrollCollectionView: Bool = (scrollView.className == "UICollectionView") // Pageスクロール中はCollectionViewスクロールを禁止 self.collectionView.isScrollEnabled = isScrollCollectionView if isScrollCollectionView { ... // PageViewをスクロール時 } else { // スワイプ前の位置からどれだけ移動したか計算 let changePageX = self.startPointPageX - scrollView.contentOffset.x // 1枚のページ内で移動した割合をCellの移動割合に let changeCollectionX = self.cellWidth * (changePageX / App.windowWidth) // 高速スクロールを封じる if abs(changePageX) > 0 && abs(changePageX) < App.windowWidth { self.notScrollView.isHidden = false } // 計算した分CollectionViewをスクロール if changeCollectionX != 0 { self.collectionView.contentOffset.x = self.startPointCollectionX - changeCollectionX } } ... }これでページをスワイプ中に、指の動きに合わせてCellも追従します。
欠点として、ページを高速で切り替えると、CollectionViewのループ処理と競合して
CollectionViewの追従がズレてしまう問題が発生しました。追従時のみUIPageViewControllerの上に透明なUIViewを表示することで
ページの高速スクロールを封じることができましたが、この辺りもっとイケてる実装にできないか..。タブの色味を追従に合わせる
画面中央に来たCellのタイトルを青く染めます。またタブ下のバーの横幅を変更します。
画面が切り替わったら色が変わるのではなく、中央に来たら変わるのがポイント。PageViewController.swiftprivate let pagelist: [String] = ["A", "BBBB", "CCC", "DDDDDD"] private var selectedCollectionNum:Int! // Cellの色を更新するメソッド private func changeCellColor(_ indexPath: IndexPath? = nil) { // 一旦全てのCell色をリセット for cell in self.collectionView.visibleCells { if let cell = cell as? BarCollectionViewCell { // カスタムCell内に配置しているLabelを灰色に cell.setSelectedCell(false) } } // 更新するCellが存在しなければreturn guard let indexPath = indexPath else { self.selectedCollectionNum = nil return } guard let cell = self.collectionView.cellForItem(at: indexPath) as? BarCollectionViewCell else { self.selectedCollectionNum = nil return } // 選択したCell番号を保持 self.selectedCollectionNum = indexPath.row // バーの横幅を更新 self.changeSelectBar(indexPath.row % self.pagelist.count) // カスタムCell内に配置しているLabelを青色に cell.setSelectedCell(true) } private func changeSelectBar(_ nextPageNum: NSInteger) { // バーの横幅を、タイトルの文字数に合わせて計算 let titleWidth: CGFloat = (CGFloat(self.pagelist[nextPageNum].count) * 15) + 14 UIView.animate( withDuration: 0.25, animations: { self.selectBarView.frame.size = CGSize(width: titleWidth, height: 3) self.selectBarView.center.x = App.windowWidth / 2 }) }BarCollectionViewCell.swiftclass BarCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var categoryTitleLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() // Initialization code } func setTitle(_ title: String) { self.categoryTitleLabel.text = title setSelectedCell(false) } // ラベルの色を切り替え func setSelectedCell(_ select: Bool) { self.categoryTitleLabel.textColor = select ? UIColor.blue : UIColor.gray } }selectBarViewは、タブ下に配置しているUIViewです。
本メソッドは、ページスクロール前、スクロール中、スクロール後、タブ選択時に呼ばれます。
具体的には以下の箇所。
- scrollViewDidScroll
- scrollViewDidEndDragging
- scrollViewDidEndDecelerating
- scrollViewWillBeginDragging // 全Cellの色味をリセットするため
- collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
選択したCell番号を、selectedCollectionNumに保持している理由は
scrollViewDidScrollで本メソッドが何度も無駄に呼ばれるのを防ぐため。PageViewController.swiftfunc scrollViewDidScroll(_ scrollView: UIScrollView) { ... // cellの色味を更新 let center = self.view.convert(self.collectionView.center, to: self.collectionView) guard let indexPath = self.collectionView.indexPathForItem(at: center) else { return } if self.selectedCollectionNum != indexPath.row { self.changeCellColor(indexPath) } }scrollViewDidScroll内で中央のCellを算出後、CellのindexPath.rowとselectedCollectionNumを比較し
異なっている時だけメソッドを呼んでいます。まとめ
実装の詳細は、Githubのサンプルコードからどうぞ。
LoopPageDemoページの高速スクロールはできませんが、ライブラリ不使用でそれなりのUIになりました。
もっとこうした方が良い等アドバイスがあれば、ぜひ (#・ω・)参考サイト
- 投稿日:2019-06-07T10:33:05+09:00
SwiftUIのTabbarで遊んでみる
SwiftUIをさわり始めて数日ですが、今までのやり方で「どうやってやるの?」的なことが多くあると思いますが、今回扱うTabbarも色々変わっっているので軽くまとめます。
あと、Viewの背景色変更する方法を暫定ですがいい感じにできたのでそこも紹介します。
※スクショは自粛で〜
First Code
Tabbarでの構成でプロジェクトを作成するとこんな感じにContentViewを作成してくれます。
まずはこれだけを見ていきましょう?
struct ContentView : View { @State private var selection = 0 var body: some View { TabbedView(selection: $selection){ Text("First View") .font(.title) .tabItemLabel(Image("first")) .tag(0) Text("Second View") .font(.title) .tabItemLabel(Image("second")) .tag(1) } } }SetTabItem
タブバーのアイテムを設定してあげています
画像なりテキストなりfunc tabItemLabel<V>(V) -> _ModifiedContent<TabbedView<SelectionValue, Content>, _TraitWritingModifier<AnyView?>>Sets the tab item to be used for this content.
Configuring Switch View
要調査ですがタグを振ってあげないとうまく切り替わってくれないです
func tag<V>(V) -> _AutoResultView<TabbedView<SelectionValue, Content>>Viewの切り替え
ContentView.swift
TabbedView(selection: $selection) { FirstView() .tabItemLabel(Image("first")) .tag(0) SecondView() .tabItemLabel(Image("second")) .tag(1) } .onAppear { print(“onAppear”) }FirstView
struct FirstView : View { var body: some View { Text("First") } }SecondView
struct SecondView : View { var body: some View { Text(“Second") } }Append self view background color
View全体に背景色を追加する方法ですね
正しいやり方か分からんけど、ZStackを使うのはわかったけどSpacerの挙動がイマイチ理解できない、、
var body: some View { ZStack { Spacer() .position(.zero) Text("First") .color(Color.white) }.background(Color.red) .edgesIgnoringSafeArea(.all) }タブバーで表示されるViewを画面全体に表示
.edgesIgnoringSafeArea(.all)だとタブバー見えなくなちゃったので上だけで
struct ContentView : View { @State private var selection = 0 var body: some View { TabbedView(selection: $selection) { FirstView() .tabItemLabel(Image("first")) .tag(0) SecondView() .tabItemLabel(Image("second")) .tag(1) } .onAppear { print("onAppear") } .edgesIgnoringSafeArea(.top) } }
- 投稿日:2019-06-07T10:33:05+09:00
SwiftUIのTabbedViewで遊んでみる
SwiftUIをさわり始めて数日ですが、今までのやり方で「どうやってやるの?」的なことが多くあると思いますが、今回扱うTabbarも色々変わっっているので軽くまとめます。
あと、Viewの背景色変更する方法を暫定ですがいい感じにできたのでそこも紹介します。
※スクショは自粛で〜
First Code
Tabbarでの構成でプロジェクトを作成するとこんな感じにContentViewを作成してくれます。
まずはこれだけを見ていきましょう?
struct ContentView : View { @State private var selection = 0 var body: some View { TabbedView(selection: $selection){ Text("First View") .font(.title) .tabItemLabel(Image("first")) .tag(0) Text("Second View") .font(.title) .tabItemLabel(Image("second")) .tag(1) } } }SetTabItem
タブバーのアイテムを設定してあげています
画像なりテキストなりfunc tabItemLabel<V>(V) -> _ModifiedContent<TabbedView<SelectionValue, Content>, _TraitWritingModifier<AnyView?>>Sets the tab item to be used for this content.
Configuring Switch View
要調査ですがタグを振ってあげないとうまく切り替わってくれないです
func tag<V>(V) -> _AutoResultView<TabbedView<SelectionValue, Content>>Viewの切り替え
ContentView.swift
TabbedView(selection: $selection) { FirstView() .tabItemLabel(Image("first")) .tag(0) SecondView() .tabItemLabel(Image("second")) .tag(1) } .onAppear { print(“onAppear”) }FirstView
struct FirstView : View { var body: some View { Text("First") } }SecondView
struct SecondView : View { var body: some View { Text(“Second") } }Append self view background color
View全体に背景色を追加する方法ですね
正しいやり方か分からんけど、ZStackを使うのはわかったけどSpacerの挙動がイマイチ理解できない、、
var body: some View { ZStack { Spacer() .position(.zero) Text("First") .color(Color.white) }.background(Color.red) .edgesIgnoringSafeArea(.all) }タブバーで表示されるViewを画面全体に表示
.edgesIgnoringSafeArea(.all)だとタブバー見えなくなちゃったので上だけで
struct ContentView : View { @State private var selection = 0 var body: some View { TabbedView(selection: $selection) { FirstView() .tabItemLabel(Image("first")) .tag(0) SecondView() .tabItemLabel(Image("second")) .tag(1) } .onAppear { print("onAppear") } .edgesIgnoringSafeArea(.top) } }
- 投稿日:2019-06-07T08:42:38+09:00
【iOS】対応必須かも?Sign In with Appleまとめ(第一報)
Sign In with Appleとは?
今年のWWDCで発表された
Appleアカウントでアプリの
サインアップやサインインができる機能です。対応必須?
様々なニュースで下記のようなことが書かれており
https://japan.cnet.com/article/35138002/レビューガイドラインを見る限り
サードパーティーのログイン連携をしているアプリは
Sign In with Appleの実装が必須になるようです。弊社のアプリでもFacebookログインなどを使用しており
今後対応が必要になるだろうと思い
まずどういうものか知るために
サンプルコードとセッション動画の内容から
現状わかったことをまとめてみました。公式のサイトはこちら
https://developer.apple.com/sign-in-with-apple/レビューガイドラインの更新情報はこちら
https://developer.apple.com/news/?id=06032019jIntroducing Sign In with Apple(セッション動画)
https://developer.apple.com/videos/play/wwdc2019/706/ちなみに
ベータテストは今夏に
正式版は今年の終わり近くに利用可能になる予定のようです。Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.※
Xcodeの画面などはNDAの関係があるため
正式版になったあとに追加させていただきます使用しているものは
公開されているスライド、ドキュメントやサンプルコードを使用していますが
もし不適切な箇所がございましたら
ご指摘いただけますと幸いですSign In with Appleの特徴
早く、簡単にアプリへの
サインアップやサインインができるとのことです。主な特徴として下記の5つの点が挙げられていました。
合理化されたアカウントセットアップ
すでにAppleのアカウントへは
ログイン状態にあることが多いので
タップだけでセットアップが完了できます。また
デバイスをまたがって同じ認証情報が使用可能です。認証されたメールアドレス
Appleがすでに認証しているので
メールを受け取って->リンクをタップする
などの余計な認証プロセスが必要になることはありません。Hide My Email
実際のメールアドレスとは異なる
下記のようなメールアドレスを使うことができ
アプリに個人情報を渡すことを避けることができます。もちろん本物のメールアドレスと同じように使えます。
※ Appleがメール内容を保持するということはないとも言っていました。
ビルトインのセキュリティ
パスワードを忘れたとしても
Appleアカウントでは2段階認証を使っているため
追加のパスワード認証プロセスや入力が必要ありません。Anti-Fraud(詐欺対策)
ユーザが疑わしいかどうかをシステムがサポートしてくれます。
内部的な情報とアカウント情報を照らし合わせて判断しているようです。
マルチプラットフォーム
iOS macOS iPadOS watchOS Javascript
実装することが可能です。処理の流れ
下記の様な手順で行っていきます。
具体的にどのような処理を行っていくか
ここからはサンプルのコードで見ていきたいと思います。Sign in with Appleを使用するためには
AuthenticationServices
というフレームワークを用います。https://developer.apple.com/documentation/authenticationservices
これを使用することで
- AppleIDを用いてログインをすることができる
- サインインフローの中で保存しているパスワードを使用することができる
- OAuthのような仕組みを使用してWebブラウザとアプリ間でデータを共有することができる
- エンタープライズアプリではシングルサインオン(SSO)のような体験を提供できる
などが実現できます。
OAuthとは?
https://ja.wikipedia.org/wiki/OAuthシングルサインオンとは?
https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%B3%E3%82%B0%E3%83%AB%E3%82%B5%E3%82%A4%E3%83%B3%E3%82%AA%E3%83%B3では実際の中身に関してはサンプルのコードから見ていきたいと思います。
Xcodeの設定をする(Sign In with Apple Entitlementを用意する)
まずはXcodeの設定が必要です。
targetを選択しSigning&Capabilitiesの中から
Sing in With Appleを選択します。サインアップボタンを用意する
ASAuthorizationAppleIDButton
を使用します。標準のものをそのまま使用することも可能です。
カスタマイズも可能ですが、
その際にはデザインガイドラインに従う必要があります。デザインガイドラインはこちら
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/
ASAuthorizationAppleIDButton
https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidbuttonAppleにユーザ情報を取得するためのリクエストを送る
次にユーザの情報を取得するために
Appleへリクエストを送ります。ユーザにタップされた際の処理は下記のようになります。
@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() }ここで出てくるいくつかのクラスについて見ていきます。
ASAuthorizationAppleIDProvider
ASAuthorizationAppleIDProvider
と
ASAuthorizationAppleIDRequest
を用いて
ユーザのAppleIDを元に
アプリが認証済みかどうかのリクエストを作成します。let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email]
fullName
と
オプションで指定することができます。
ASAuthorizationAppleIDProvider
https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovider
ASAuthorizationAppleIDRequest
https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidrequestASAuthorizationController
次に上記で作成したリクエストを使って
ASAuthorizationController
を初期化し
performRequests
でAppleへリクエストを送ります。let controller = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.presentationContextProvider = self authorizationController.performRequests()
ASAuthorizationController
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller
performRequests
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller/3153047-performrequestsASAuthorizationControllerDelegate
上記の中で
authorizationController.delegate = selfとありますが
これは
ASAuthorizationControllerDelegate
というプロトコルです。
ASAuthorizationControllerDelegate
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegateこれを設定するとリクエストのレスポンスを下記のデリゲートで取得することができます。
extension LoginViewController: ASAuthorizationControllerDelegate { func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { // ASAuthorizationAppleIDCredentialの場合 if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { // 取得できた情報 let userIdentifier = appleIDCredential.user let fullName = appleIDCredential.fullName let email = appleIDCredential.email // 取得した情報を元にアカウントの作成などを行う // ASPasswordCredentialの場合(※あとで紹介します※) } else if let passwordCredential = authorization.credential as? ASPasswordCredential { // 既存のiCloud Keychainクレデンシャル情報 let username = passwordCredential.user let password = passwordCredential.password // 取得した情報を元にアカウントの作成などを行う } } }
ASAuthorizationAppleIDProvider
のリクエストが成功した場合
取得できる情報はASAuthorizationAppleIDCredential
です。
ASAuthorizationAppleIDCredential
https://developer.apple.com/documentation/authenticationservices/asauthorizationこの中の情報を使ってアカウントの作成などを行います。
authorizationController(controller:didCompleteWithAuthorization:)
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate/3153050-authorizationcontrollerエラーの場合は下記のデリゲートメソッドが呼ばれます。
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { // Handle error. }
authorizationController(controller:didCompleteWithError:)
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate/3153051-authorizationcontrollerASPasswordCredentialについて
上記のデリゲートメソッドの中で
} else if let passwordCredential = authorization.credential as? ASPasswordCredential {という分岐が出てきていますが
これはiCloud KeyChainのパスワード情報をリクエストした場合に
取得できる情報です。
ASPasswordCredential
https://developer.apple.com/documentation/authenticationservices/aspasswordcredential実はサンプルコードの中では
ユーザのタップする場合以外にも
viewDidAppear
の中でも
認証情報のリクエストを送っています。func performExistingAccountSetupFlows() { // Prepare requests for both Apple ID and password providers. let requests = [ASAuthorizationAppleIDProvider().createRequest(), ASAuthorizationPasswordProvider().createRequest()] // Create an authorization controller with the given requests. let authorizationController = ASAuthorizationController(authorizationRequests: requests) authorizationController.delegate = self authorizationController.presentationContextProvider = self authorizationController.performRequests() }注目点は下記で
let requests = [ASAuthorizationAppleIDProvider().createRequest(), ASAuthorizationPasswordProvider().createRequest()]
ASAuthorizationPasswordProvider
を使っています。
ASAuthorizationPasswordProvider
https://developer.apple.com/documentation/authenticationservices/asauthorizationpasswordproviderこのリクエストを送ることで
iCloud Keychainのクレデンシャル情報を使って
アカウントの作成などが可能になります。※複数設定した場合の優先順位などに関しては何も言及がありませんでしたので
これは今後調べたいと思います。今回は割愛させていただきますが
シングルサインオン用の入り口もあります。
ASAuthorizationSingleSignOnProvider
https://developer.apple.com/documentation/authenticationservices/asauthorizationsinglesignonprovider
ASAuthorizationSingleSignOnCredential
https://developer.apple.com/documentation/authenticationservices/asauthorizationsinglesignoncredentialASAuthorizationControllerPresentationContextProviding
リクエストを送る際に下記のコードがありました。
authorizationController.presentationContextProvider = selfこれは
ASAuthorizationControllerPresentationContextProviding
プロトコルで
システムが認証プロセスをユーザに提供するためのUIWindowの指定をします。
いくつかのリクエストでは必須となっているようです。
ASAuthorizationControllerPresentationContextProviding
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerpresentationcontextprovidingサンプルコードでは
下記のようにアプリのUIWindow
を設定しています。extension LoginViewController: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return self.view.window! } }
presentationAnchor(for:)
https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerpresentationcontextproviding/3237228-presentationanchorこのようにしてユーザのアカウント作成ができます。
クレデンシャル情報の変更に対応する
例えば
- アプリでAppleIDの使用をやめた
- デバイスでサインアウトをした
などが起きた場合への対応も必要になります。
サンプルのAppDelegateを見ていきます。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let appleIDProvider = ASAuthorizationAppleIDProvider() appleIDProvider.getCredentialState(forUserID: KeychainItem.currentUserIdentifier) { (credentialState, error) in switch credentialState { case .authorized: // Apple IDクレデンシャルが妥当だった場合 break case .revoked: // Apple IDクレデンシャルが無効だった場合 break case .notFound: // Apple IDクレデンシャル情報がなかった場合 // サインアップのフローを開始する default: break } } return true }発表のスライドとはやや異なりますが、
Keychainに保存しているcurrentUserIdentifier
を
getCredentialState(forUserID:completion:)
の引数として設定し
クレデンシャル情報を取得しています。
getCredentialState(forUserID:completion:)
https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovider/3175423-getcredentialstateここで得られる
credentialState
が
ASAuthorizationAppleIDProvider.CredentialState
です。enum CredentialState : Int { case authorized = 1 case notFound = 2 case revoked = 3 }
ASAuthorizationAppleIDProvider.CredentialState
https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovider/credentialstateこの
revoked
の際に対応していきます。また発表スライドでは
通知を受け取ることもできるとあり
下記のような形で
クレデンシャル情報が無効になった時の処理ができます。// Register for revocation notification let center = NotificationCenter.default let name = NSNotification.Name.ASAuthorizationAppleIDProviderCredentialRevoked let observer = center.addObserver(forName: name, object: nil, queue: nil) { (Notification) in // サインアウトして、再度サインインフローを表示するなど }
ASAuthorizationAppleIDProviderCredentialRevoked
https://developer.apple.com/documentation/foundation/nsnotification/name/3175424-asauthorizationappleidprovidercrサインイン後はワンタップでサインインできる
サインアップのプロセスを見てきましたが
Appleアカウントでアプリの認証がされていることで
アプリ起動時に認証されているかどうかがわかり
即座にサインイン状態だと判定することが可能です。
※ 上記AppDelegateのメソッド内の分岐をご参照ください。これはユーザにとっても
不要な手順が省かれ楽になり
個人情報を入力するリスクを避けられます。最後に
サンプルコードとセッションの情報から
まずまとめてみました。まだまだ理解ができていない部分や知らないことは
多々あると思いますので
更新情報がありましたら追記したいと思います。
- 投稿日:2019-06-07T00:48:27+09:00
iOS13のScene-Basedライフサイクル(UISceneDelegate)
はじめに
iOS12のアプリライフサイクル(AppDelegate)を調べていたら、
iOS13のscene-basedだとライフサイクルが変わるようなので、
資料から読み取れる概要を書いていきます。
iOS12のAppDelegate相当が、 scene-basedの場合には、
UISceneDelegateになるようです。Scene-Basedライフサイクル(ios13以降)
大きな考え方は、ios12のApp-Basedライフサイクルとそれほど変わらなさそう。
フォアグランドに遷移する時には、以下のメソッドが呼ばれる。
- func sceneWillEnterForeground(UIScene)
- func sceneDidBecomeActive(UIScene)バックグランドに遷移する時には、以下のメソッドが呼ばれる。
- func sceneWillResignActive(UIScene)
- func sceneDidEnterBackground(UIScene)App-Basedライフサイクル(参考)
参考文献