- 投稿日:2020-08-30T23:35:06+09:00
Swift で日本語を含む URL を扱う
日本語を含む文字列を URL に変換する時に値が nil になることがあると思います。これは、URI において使用できない文字列を URL として変換しようとしている時に発生します。Google などの検索バーでは、このような使用できない文字列を自動でパーセントエンコーディングしているので正常に検索を行うことができます。自分もたまに、エンコーディングを忘れてしまうのでこれを機に気をつけて行こうかと思います。
文字列をエンコード
今回は楽天市場の商品 URL を取得するという設定でエンコーディングを行っていきます。
let itemString = "スコーピオン" let itemEncodeString = itemString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) let urlString = "https://search.rakuten.co.jp/search/mall/\(itemEncodeString!)" print("itemEncodeString: \(itemEncodeString!)") // itemEncodeString: %E3%82%B9%E3%82%B3%E3%83%BC%E3%83%94%E3%82%AA%E3%83%B3 print("Result URL: \(URL(string: urlString)!.absoluteString)") // Result URL: https://search.rakuten.co.jp/search/mall/%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%94%E3%82%AA%E3%83%B3上記のようにエンコードすることができました?
.addingPercentEncoding(withAllowedCharacters: CharacterSet)
にセットするCharacterSetですが、今回の場合は URL の Path にセットされる文字列に日本語が含まれている可能性があったので.urlPathAllowed
という CharacterSet を使用しています。URL String 全体に対してエンコーディングをかける場合は、.urlFragmentAllowed
もしくは.urlQueryAllowed
を使用すればそれなりに正しくは動きますが、個人的にはお勧めしません。理由としては、それぞれの CharacterSet で守備範囲としている URL コンポーネントが違うからです。例で言うと下記のように、パスの文字列で?
を含む URL String 全体を.urlQueryAllowed
を使ってエンコードした場合、?
は文字列とみなされずエンコードされません。つまり、結果として取得した URL が正確でない可能性があります。そのため、使う用途に分けて URL コンポーネントそれぞれをエンコードするのがベストでしょう。let urlString = "https://search.rakuten.co.jp/search/mall/スコーピオ?ン" let encodeUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) print("Result URL: \(URL(string: encodeUrlString!)!.absoluteString)") // Result URL: https://search.rakuten.co.jp/search/mall/%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%94%E3%82%AA?%E3%83%B3URL コンポーネント別の CharacterSet 一覧は下記になります。
参考
- 投稿日:2020-08-30T22:22:47+09:00
UITableViewにUIButtonを追加する
UITableViewの追加方法と、Cellにボタンを追加する方法を記載します。
UITableViewの作成
UITableView(frame: CGRect, style: UITableView.Style)でUITableViewを作成します。
CGRect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat)でテーブルのサイズを指定し、
self.view.addSubview()でテーブルを画面に追加します、var tableView: UITableView = UITableView() override func viewDidLoad() { super.viewDidLoad() // 画面の横の長さ let width = self.view.frame.size.width // 画面の縦の長さ let height = self.view.frame.size.height tableView = UITableView(frame: self.view.frame, style: UITableView.Style.grouped) tableView.dataSource = self tableView.delegate = self tableView.sizeToFit() // tableViewのサイズ指定 tableView.frame = CGRect(x:0, y:50, width:Int(width), height: Int(height)) // 追加 self.view.addSubview(tableView) }UITableViewを作成するばあ、row数とsection数の指定も必要になります。
// section数 public func numberOfSections(in tableView: UITableView) -> Int { return 1 } // row数 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 9 }Cellの中身を追加
UITableViewのCellを内容を追加します。
Cellに、UItextField、UIButton等の追加ができます。
追加時にタグにindexPathを入れることで、選択時にどこを選択しているのか判断できます。// Cellの高さ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 50 } // Cellの内容 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCell(withIdentifier: "Cell") if cell == nil { cell = UITableViewCell(style: .value1, reuseIdentifier: "Cell") } else { self.view.willRemoveSubview(cell!) cell = UITableViewCell(style: .value1, reuseIdentifier: "Cell") } let width = self.view.frame.size.width // UITextField let textfi = UITextField() textfi.delegate = self textfi.tag = indexPath.row textfi.frame = CGRect(x:50, y:0, width:width - 50, height: 50) cell?.addSubview(textfi) // UIButton let addbutton = UIButton() addbutton.tag = indexPath.row // 押下時動作 addbutton.addTarget(self, action: #selector(buttonEvemt), for: UIControl.Event.touchUpInside) addbutton.frame = CGRect(x:0, y:0, width:50, height: 50) cell?.addSubview(addbutton) return cell! }Cellの選択時の動作
Cellを選択してときの動作を追加できます。
UIButton、UITextField等は画面に追加したときと同じ用に記載すればいいですが、
どのCellを選択しているかわからないため、tagに入れたindexPath.rowでどれを選択しているか
判断できるようにする必要があります。// Cell選択時動作 func tableView(_ table: UITableView, didSelectRowAt indexPath: IndexPath){ } // UIButtonを選択した時 @objc func buttonEvemt(_ sender: UIButton) { print("buttonTag", sender.tag) } // UITextFieldを選択した時 func textFieldDidBeginEditing(_ textField: UITextField) { print("start",textField) print("tag",textField.tag) } // UITextFieldを離した時 func textFieldDidEndEditing(_ textField: UITextField) { print("end",textField) print("tag",textField.tag) }
- 投稿日:2020-08-30T20:41:04+09:00
SwiftUIでいのちを輝かせる
SwiftUIで大阪万博のいのちの輝き君を作成してみました。
※ 色はシステムカラーを使用しているため、本来の色とは異なります。
1. 赤い背景を作成
import SwiftUI struct ContentView: View { var body: some View { GeometryReader { geometry in ZStack { inochiRed.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center) } } } var inochiRed: some View { GeometryReader { geometry in Group { Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.21, height: geometry.size.height * 0.21, alignment: .center) .position(x: geometry.size.width * 0.43, y: geometry.size.width * 0.124) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.20, height: geometry.size.height * 0.20, alignment: .center) .position(x: geometry.size.width * 0.296, y: geometry.size.width * 0.231) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.20, height: geometry.size.height * 0.20, alignment: .center) .position(x: geometry.size.width * 0.135, y: geometry.size.width * 0.328) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.19, height: geometry.size.height * 0.19, alignment: .center) .position(x: geometry.size.width * 0.273, y: geometry.size.width * 0.390) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.25, height: geometry.size.height * 0.25, alignment: .center) .position(x: geometry.size.width * 0.169, y: geometry.size.width * 0.529) } Group { Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.12, height: geometry.size.height * 0.22, alignment: .center) .position(x: geometry.size.width * 0.255, y: geometry.size.width * 0.685) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.18, height: geometry.size.height * 0.18, alignment: .center) .position(x: geometry.size.width * 0.347, y: geometry.size.width * 0.819) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.27, height: geometry.size.height * 0.27, alignment: .center) .position(x: geometry.size.width * 0.535, y: geometry.size.width * 0.849) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.30, height: geometry.size.height * 0.25, alignment: .center) .position(x: geometry.size.width * 0.714, y: geometry.size.width * 0.695) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.19, height: geometry.size.height * 0.30, alignment: .center) .position(x: geometry.size.width * 0.729, y: geometry.size.width * 0.477) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.30, height: geometry.size.height * 0.15, alignment: .center) .position(x: geometry.size.width * 0.753, y: geometry.size.width * 0.327) Ellipse() .fill(Color.red) .frame(width: geometry.size.width * 0.28, height: geometry.size.height * 0.28, alignment: .center) .position(x: geometry.size.width * 0.653, y: geometry.size.width * 0.165) } } } }2. 白い目を作成
var body: some View { GeometryReader { geometry in ZStack { inochiRed.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center) inochiWhite.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center) } } } var inochiWhite: some View { GeometryReader { geometry in Ellipse() .fill(Color.white) .frame(width: geometry.size.width * 0.09, height: geometry.size.height * 0.09, alignment: .center) .position(x: geometry.size.width * 0.105, y: geometry.size.width * 0.310) Ellipse() .fill(Color.white) .frame(width: geometry.size.width * 0.10, height: geometry.size.height * 0.10, alignment: .center .position(x: geometry.size.width * 0.197, y: geometry.size.width * 0.551) Ellipse() .fill(Color.white) .frame(width: geometry.size.width * 0.085, height: geometry.size.height * 0.085, alignment: .center) .position(x: geometry.size.width * 0.520, y: geometry.size.width * 0.900) Ellipse() .fill(Color.white) .frame(width: geometry.size.width * 0.135, height: geometry.size.height * 0.120, alignment: .center) .position(x: geometry.size.width * 0.773, y: geometry.size.width * 0.713) Ellipse() .fill(Color.white) .frame(width: geometry.size.width * 0.11, height: geometry.size.height * 0.11, alignment: .center) .position(x: geometry.size.width * 0.685, y: geometry.size.width * 0.105) } }3.青い目を作成
var body: some View { GeometryReader { geometry in ZStack { inochiRed.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center) inochiWhite.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center) inochiBlue.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center) } } } var inochiBlue: some View { GeometryReader { geometry in Ellipse() .fill(Color.blue) .frame(width: geometry.size.width * 0.045, height: geometry.size.height * 0.045, alignment: .center) .position(x: geometry.size.width * 0.085, y: geometry.size.width * 0.30) Ellipse() .fill(Color.blue) .frame(width: geometry.size.width * 0.045, height: geometry.size.height * 0.045, alignment: .center) .position(x: geometry.size.width * 0.193, y: geometry.size.width * 0.522) Ellipse() .fill(Color.blue) .frame(width: geometry.size.width * 0.040, height: geometry.size.height * 0.040, alignment: .center) .position(x: geometry.size.width * 0.520, y: geometry.size.width * 0.923) Ellipse() .fill(Color.blue) .frame(width: geometry.size.width * 0.050, height: geometry.size.height * 0.050, alignment: .center) .position(x: geometry.size.width * 0.812, y: geometry.size.width * 0.700) Ellipse() .fill(Color.blue) .frame(width: geometry.size.width * 0.050, height: geometry.size.height * 0.050, alignment: .center) .position(x: geometry.size.width * 0.705, y: geometry.size.width * 0.080) } }完成!!
参考
- 投稿日:2020-08-30T20:36:49+09:00
謎のinsertArrangedSubviewでクラッシュする問題
筆者が開発しているiOSのアプリで、App Store Connect経由でクラッシュログが繰り返し送られてくるものの、再現方法も解決方法もずっと謎であった不具合が解決したのでメモとして残しておきます。
問題の内容
送られてくるクラッシュログは以下のようなものでした。
Last Exception Backtrace: 0 CoreFoundation 0x18d2d75b4 __exceptionPreprocess + 220 (NSException.m:199) 1 libobjc.A.dylib 0x1a12c742c objc_exception_throw + 60 (objc-exception.mm:565) 2 CoreFoundation 0x18d1d3aac +[NSException raise:format:] + 112 (NSException.m:155) 3 UIKitCore 0x19001b6d4 -[UIStackView insertArrangedSubview:atIndex:] + 184 (UIStackView.m:103) 4 UIKitCore 0x18f1723e4 -[_UIButtonBar _layoutBar] + 2576 (UIButtonBar.m:555) 5 UIKitCore 0x18f175b38 __42-[_UIButtonBarStackView updateConstraints]_block_invoke + 52 (UIButtonBar.m:1293) 6 UIKitCore 0x190112468 +[UIView(Animation) performWithoutAnimation:] + 104 (UIView.m:13927) 7 UIKitCore 0x18f175ad8 -[_UIButtonBarStackView updateConstraints] + 120 (UIButtonBar.m:1292) 8 UIKitCore 0x19003458c -[UIView(AdditionalLayoutSupport) _sendUpdateConstraintsIfNecessaryForSecondPass:] + 484 (NSLayoutConstraint_UIKitAdditions.m:0) 9 UIKitCore 0x190034a44 -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededCollectingViews:forSecondPass:] + 948 (NSLayoutConstraint_UIKitAdditions.m:4409) 10 UIKitCore 0x190034924 -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededCollectingViews:forSecondPass:] + 660 (NSLayoutConstraint_UIKitAdditions.m:4390) 11 UIKitCore 0x190034924 -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededCollectingViews:forSecondPass:] + 660 (NSLayoutConstraint_UIKitAdditions.m:4390) 12 CoreAutoLayout 0x1a154c060 -[NSISEngine withBehaviors:performModifications:] + 88 (NSISEngine.m:1917) 13 UIKitCore 0x1900351f4 __100-[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededWithViewForVariableChangeNotifications:]_block_invoke + 116 (NSLayoutConstraint_UIKitAdditions.m:4455) 14 UIKitCore 0x190033b30 -[UIView(AdditionalLayoutSupport) _withUnsatisfiableConstraintsLoggingSuspendedIfEngineDelegateExists:] + 128 (NSLayoutConstraint_UIKitAdditions.m:4154) 15 UIKitCore 0x190034db4 -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededWithViewForVariableChangeNotifications:] + 188 (NSLayoutConstraint_UIKitAdditions.m:4454) 16 UIKitCore 0x190035cac -[UIView(AdditionalLayoutSupport) _updateConstraintsAtEngineLevelIfNeededWithViewForVariableChangeNotifications:] + 436 (NSLayoutConstraint_UIKitAdditions.m:4707) 17 UIKitCore 0x19010c028 -[UIView _updateConstraintsAsNecessaryAndApplyLayoutFromEngine] + 404 (UIView.m:12304) 18 UIKitCore 0x18f2659f8 -[UIToolbar layoutSubviews] + 56 (UIToolbar.m:712) 19 UIKitCore 0x19011f7dc -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2500 (UIView.m:17460) 20 QuartzCore 0x190627464 -[CALayer layoutSublayers] + 296 (CALayer.mm:10129) 21 QuartzCore 0x190627920 CA::Layer::layout_if_needed(CA::Transaction*) + 524 (CALayer.mm:9996) 22 QuartzCore 0x19063bd48 CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 144 (CALayer.mm:2478) 23 QuartzCore 0x1905833e4 CA::Context::commit_transaction(CA::Transaction*, double, double*) + 416 (CAContextInternal.mm:2380) 24 QuartzCore 0x1905ae6dc CA::Transaction::commit() + 732 (CATransactionInternal.mm:449) 25 QuartzCore 0x1905afa3c CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 96 (CATransactionInternal.mm:925) 26 CoreFoundation 0x18d253454 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 36 (CFRunLoop.c:1799) 27 CoreFoundation 0x18d24d868 __CFRunLoopDoObservers + 576 (CFRunLoop.c:1912) 28 CoreFoundation 0x18d24de18 __CFRunLoopRun + 1056 (CFRunLoop.c:2953) 29 CoreFoundation 0x18d24d4cc CFRunLoopRunSpecific + 600 (CFRunLoop.c:3242) 30 GraphicsServices 0x1a3c31820 GSEventRunModal + 164 (GSEvent.c:2259) 31 UIKitCore 0x18fbf2a28 -[UIApplication _run] + 1072 (UIApplication.m:3270) 32 UIKitCore 0x18fbf8104 UIApplicationMain + 168 (UIApplication.m:4739) 33 KifuBox 0x102e6c2b0 main + 68 (WaitingViewController.swift:12) 34 libdyld.dylib 0x18cf14e60 start + 4厄介な点の1つとして、自分が直接書いたソースコードはスタック中にはなく、何をトリガに起きているのかわからないことがありました。
最終的にはUIStackView
のinsertArrangedSubview
で落ちており、スタックの途中には_UIButtonBarStackView
などが出ているので、多分UIBarButtonItem
に関連する何かだとは思うのですが、どこで問題が起きているのか全くわかりませんでした。発生の傾向を見ると、iOS13.4以降でしか起きていないようで、それがiOS13.4以降でしか発生しない現象なのか、たまたま時期的な関係でそう見えているのかはよくわかりませんでした。
原因と解決方法
結論としては、Apple Developers Forumにあった以下の記事が当たりでした。
https://developer.apple.com/forums/thread/130038上記記事では
UIAlertController
だそうですが、筆者のアプリでは画面下部のUIToolbar
にUIBarButtonItem
を追加する際に、同じオブジェクトが含まれていると、iOS13.4以降ではクラッシュするという問題でした。(iOS13.3以前では同じコードでも問題は発生しない)MyViewController.swiftself.setToolbarItems([filterButton, fixSpace1, dummyIcon, flexSpace1, filterStateButton, flexSpace2, dummyIcon, fixSpace2, dummyIcon], animated: true)上記のソースコードが問題で、ツールバーにボタンを並べる際のレイアウト調整のために、見えないダミーのボタン(
dummyIcon
)を3つ追加していたのですが、その3つが同じオブジェクトであったためにクラッシュしていました。以下のように3つ別のオブジェクトにすることで解決しました(オブジェクトの生成コード等は省略)
MyViewController.swiftself.setToolbarItems([filterButton, fixSpace1, dummyIcon1, flexSpace1, filterStateButton, flexSpace2, dummyIcon2, fixSpace2, dummyIcon3], animated: true)手がかりが
insertArrangedSubview
くらいしかなくて難航したので、少しでもネット上で情報が見つかるように記事を書いてみました。
- 投稿日:2020-08-30T19:55:28+09:00
UITableViewのinsertRows()でEXC_BAD_ACCESSが発生した件
筆者が開発しているiOSのアプリで、App Store Connect経由でクラッシュログが繰り返し送られてくるものの、再現方法も解決方法もずっと謎であった不具合が解決したのでメモとして残しておきます。
問題の内容
送られてくるクラッシュログは以下のようなものでした。
Thread 0 name: Thread 0 Crashed: 0 UIKitCore 0x000000018fde4bd0 __46-[UITableView _updateWithItems:updateSupport:]_block_invoke + 60 (UITableView.m:3826) 1 UIKitCore 0x000000018fde4bd0 __46-[UITableView _updateWithItems:updateSupport:]_block_invoke + 60 (UITableView.m:3826) 2 UIKitCore 0x000000018fde4cdc __46-[UITableView _updateWithItems:updateSupport:]_block_invoke.1109 + 212 (UITableView.m:3911) 3 UIKitCore 0x000000018fde4040 -[UITableView _updateWithItems:updateSupport:] + 2036 (UITableView.m:3919) 4 UIKitCore 0x000000018fddcfe4 -[UITableView _endCellAnimationsWithContext:] + 11128 (UITableView.m:2387) 5 UIKitCore 0x000000018fdf5ad8 -[UITableView _updateRowsAtIndexPaths:withUpdateAction:rowAnimation:usingPresentationValues:] + 628 (UITableView.m:7767) 6 UIKitCore 0x000000018fdf5bec -[UITableView insertRowsAtIndexPaths:withRowAnimation:] + 172 (UITableView.m:7776) 7 KifuBox 0x000000010087fc8c GameListViewController.addGame(game:) + 1140 (GameListViewController.swift:460) 8 KifuBox 0x00000001008826bc addGame + 16 (GameListViewController.swift:436) 9 KifuBox 0x00000001008826bc GameListViewController.newPushed() + 1804 (GameListViewController.swift:628) 10 KifuBox 0x000000010088329c @objc GameListViewController.newPushed() + 28 (<compiler-generated>:0) (以下略)
UITableView
で作成した一覧で新規に行を追加した場合に、insertRows(at:with:)
でEXC_BAD_ACCESSが発生するというものです。
送られてくるクラッシュログの発生環境から見ると、iPadでのみ発生し、iPhoneでは発生していませんでした。OSのバージョンはあまり関係ないようで、11系〜13系まで幅広く発生していました。発生し始めた時期に行った修正内容でinsertRows近辺のものがないか調べましたが、あまり関係のありそうなものがありませんでした。
ネットでもいろいろ検索してみたところ、テーブルの更新部分を
beginUpdates()
〜endUpdates()
で囲うと良いという情報があったのでやってみましたが、効果はなく、それ以外に有効な情報は見つかりませんでした。再現条件と解決方法
それから紆余曲折を経て、やっと再現方法がわかりました。このアプリは
UISplitViewController
を使用して、プライマリー部分にリストを、セカンダリー部分にそのコンテンツを表示するようなものになっています。
iPadではリストとコンテンツが同時に表示されるので、わかりやすいように表示されているコンテンツに対応するリストの行を選択表示していました。
UITableView
で編集モードに入ると、選択状態が解除されてしまうため、編集モードから出た際に行をselectRow(at:animated:scrollPosition:)
で再選択するロジックを、問題が起こり始めた時期に追加していたのですが、このロジックに誤りがあり、既になくなっている行をselectRow
してしまうケースがありました。確実に再現できた例でいくと、リストに1行しかない状態で編集モードに入り、行を削除して編集モードから抜けると、既に行は1つもないにもかかわらず、row=0, section=0の行を
selectRow()
してしまっていました。この状態から新規に行を追加してinsertRows(at:with:)
を行うとクラッシュする、という現象でした。このような理由だったので、
selectRow()
が正しくなるように修正した結果、クラッシュしないようになりました。まとめ
UITableView
で存在しない行がselectRow()
されている状態でinsertRows(at:with:)
を行うとクラッシュする場合があります。
あまり発生しにくいケースだとは思いますが、ネット上にもほとんど情報が見つからなかったので、一応記事にしておきました。
- 投稿日:2020-08-30T13:11:26+09:00
App Attest APIの概要
※2020/9/1執筆→11/16全体公開(忘れていました)
App Attest APIは、iOS 14以上で使用できる、DeviceCheckの一種です。
たとえばアプリがハックされると、チートや広告の削除、有料機能の不正利用などが行われます。
それを防ぐため、デバイスで特殊な暗号鍵を生成し、サーバーが機密データにアクセスする際にAppleの認証を受けることで、アプリが正当なことを証明できます。アプリの実装方法
こちら参照。DCAppAttestServiceを用いて以下のように実装します。
1. API利用可否の判定
guard DCAppAttestService.shared.isSupported else { log.error("Unsupported") return } // サービス利用可2. 暗号鍵の生成
DCAppAttestService.shared.generateKey { keyId, error in guard error == nil, let keyId = keyId else { log.error(error.debugDescription ?? "Unknown error") return } // KeyId生成完了 }3. チャレンジの要求
サーバー(Your server)にKeyIdを送り、チャレンジ(サーバーが決めたランダム値)を受け取ります。
let challenge = // サーバーから受け取る4. keyIdとチャレンジを使って認証
let hash = Data(SHA256.hash(data: challenge)) DCAppAttestService.shared.attestKey(keyId, clientDataHash: hash) { attestation, error in if let error = error { log.error(error.debugDescription) } else if let attestation = attestation { // 認証OK/NG判定 } else { log.error("Attestation is null") } }ここまでは、アプリ起動時に1回だけ行えばよい
5. アプリの正当性の主張
以後、サーバーリクエスト毎に、アプリの正当性を主張します。
今回はチャレンジとサーバーリクエスト内容を組み合わせたハッシュを作成します。
let challenge = // サーバーから受け取る let request = [ "action": "getUser", "userId": "1234", "challenge": challenge ] guard let clientData = try? JSONEncoder().encode(request) else { return } let hash = Data(SHA256.hash(data: clientData)) service.generateAssertion(keyId, clientDataHash: hash) { assertion, error in if let error = error { log.error(error.debugDescription) } else if let assertion = assertion { // 正当性OK/NG判定 } else { log.error("Assertion is null") } }サーバーの実装方法
こちら参照。やることは以下
・(Your Appに対して)チャレンジの払い出しと保持
・(App attestに対して)認証オブジェクトの生成※サーバーの実装方法はよくわかっておらず、調査中
DeviceCheckについて
App Attest単体では、不正を行ったデバイスの特定はできないので、特定したい場合DeviceCheckと併用します。
DeviceCheckは、こちらの記事が分かりやすいです。参考
- 投稿日:2020-08-30T11:32:04+09:00
リーダブルコードで読みにくいコードを改善していく ~ コメント編
どうも、ねこきち(@nekokichi1_yos2)です。
リーダブルコード改善シリーズ、第4弾、です。
(詳しくは下記をご参照ください)
リーダブルコードで読みにくいコードを改善していく ~ 準備編
リーダブルコードで読みにくいコードを改善していく ~ 名前編
リーダブルコードで読みにくいコードを改善していく ~ 美しさ編今回は、コメントでコードを分かりやすく改善していきます。
リーダブルコードの教え
「コードからわかることはコメントしない」
・必要でもなく、無くても困らない
・同じ情報を2つも提示する必要はない
・新しい情報を提示してないなら、無価値「自分の考えを述べる」
・詳しい情報を伝えられる
・読む人にどうして欲しいかが伝わる「後で見返すためのコメント」
・コードの状態を視覚的に示す
・TODO、MUSTなどの単語「何も書かないよりマシ」
・伝えたい情報が伝わればいい
・完璧なコメントを求めなくていい「ハマりそうな罠を回避」
・コードの実行結果を伝える
・実行して確認する手間が省く
・罠を探す、罠の対処にかかる時間を短縮「記号や数字でコメント」
・計算式、関係を表す式
・例:引数->インスタンス名「簡潔で分かりやすいコメント」
・代名詞(あれ、それ、これ)は書くな
・接続詞など、余計な言葉は書くな
・曖昧で、冗長で、理解に手間取る「実例を載せる」
・コードの実行結果
・扱う値やデータがどのように処理されるかがわかる「一般的視点からコメント」
・一般人でも理解できる形
・例:〇〇が〜〜された
・実行結果を簡潔に伝えられる「キーワードや用語で効率化」
・ITやエンジニアに共通用語
・例:〇〇処理、プロパティ
・長いコメントを代替えできる変なコメントを探す
本記事を投稿する前に、あちこちのファイルに変なコメントをわざと追加しました。
ViewController
//メモリスト var memoList:Results<MemoModel>! //Realm let realm = try! Realm() //保存用の配列 var attributedTextArray = [NSAttributedString]() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) //初期化 attributedTextArray = [NSAttributedString]() //データ取得 memoList = realm.objects(MemoModel.self) //Realmから取得したデータをAttributedStringに変換していって //attributedTextArrayに追加していく if memoList.count > 0 { for i in 0...memoList.count-1 { let attributeText = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(memoList![i].data) as! NSAttributedString attributedTextArray.append(attributeText) } } //tableView更新 memoTableView.reloadData() }・メモリスト:どんなメモリストで、何に使われるのか
・Realm:Realmの何なのか?
・保存用の配列:何を保存し、どこに保存するのか?
・初期化:何を初期化するのか?
・データ取得:どこから何を取得する?
・Realmから取得したデータをAttributedStringに変換していってattributedTextArrayに追加していく:冗長で何が言いたいのか簡潔に伝わらない
・tableView更新:どのtableViewを指すのか?
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { //選択したメモのデータ let selectedMemo = memoList[indexPath.row] //TableViewで選択したメモのデータをRealmから削除する try! realm.write() { realm.delete(selectedMemo) } //tableView更新 tableView.reloadData() } /***/ override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "display" { //DisplayMemoのインスタンスを宣言 let vc = segue.destination as! DisplayMemo //DisplayMemoのプロパティに値を代入 vc.selectedMemoObject = memoList[memoTableView.indexPathForSelectedRow!.row] vc.selectedMemo_attributedText = attributedTextArray[memoTableView.indexPathForSelectedRow!.row] vc.selectedIndexPathRow = memoTableView.indexPathForSelectedRow!.row } }・選択したメモのデータ:どこで選択した?
・TableViewで選択したメモのデータをRealmから削除する:どのTableViewなのか、少し長い
・DisplayMemoのインスタンスを宣言:内容が技術的寄りで、行われている過程が分かりづらい
AddMemo
//Realmに保存するためのデータの集まり class MemoModel: Object { @objc dynamic var data: Data! @objc dynamic var identifier: String! }・Realmに保存するためのデータの集まり:具体的にどのようなデータかを説明した方がいい
override func viewDidLoad() { super.viewDidLoad() //imagePickerの設定 imagePicker.delegate = self imagePicker.sourceType = .photoLibrary }・imagePickerの設定:無くてもわかる
@IBAction func addMemo(_ sender: Any) { //必要な変数を宣言 let memoObject = MemoModel() //memoTextViewに入力したテキストをData型に変換 let archivedAttributedText = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false) //入力値を代入 memoObject.data = archivedAttributedText memoObject.identifier = String().randomString() //Realmにデータを追加 try! realm.write{ realm.add(memoObject) } //戻る self.navigationController?.popViewController(animated: true) }・必要な変数を宣言:コメントが示す変数が何なのか不明
・memoTextViewに入力したテキストをData型に変換:言い換えられないだろうか?
・入力値を代入:コメントが無くても理解できる
・Realmにデータを追加:addでデータを追加すると理解できる
・戻る:戻り先を示した方がいい
//長押しタップで画像を添付 @IBAction func attachImageGesture(_ sender: UILongPressGestureRecognizer) { //アラート let alert = UIAlertController(title: "画像を添付", message: nil, preferredStyle: .actionSheet) //アクション let action = UIAlertAction(title: "OK", style: .default) { (action) in self.dismiss(animated: true, completion: nil) self.present(self.imagePicker, animated: true, completion: nil) } //キャンセル let cancel = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil) //アラートアクションを追加 alert.addAction(action) alert.addAction(cancel) //表示 present(alert, animated: true, completion: nil) }・長押しタップで画像を添付:アラートを中継してるので、処理の流れを記述した方がいい
・アラート:アラートコントローラーの方が適切
・アクション:処理内容を示しても良い
・キャンセル:アクションとかを追加した方がいい
・アラートアクションを追加:コメントが無くても理解できる
・表示:何を表示する?
extension AddMemo: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let pickerImage = info[.originalImage] as? UIImage { //画像の圧縮やサイズ調整に必要な値 let width = pickerImage.size.width let padding = self.view.frame.width / 2 let scaleRate = width / (memoTextView.frame.size.width - padding) //圧縮した画像 let resizedImage = pickerImage.resizeImage(withPercentage: 0.1)! //インスタンスの宣言 let imageAttachment = NSTextAttachment() var imageAttributedString = NSAttributedString() //memoTextViewのテキストをAttributedStringに変換 let mutAttrMemoText = NSMutableAttributedString(attributedString: memoTextView.attributedText) //画像をNSAttributedStringに変換 imageAttachment.image = UIImage(cgImage: resizedImage.cgImage!, scale: scaleRate, orientation: resizedImage.imageOrientation) imageAttributedString = NSAttributedString(attachment: imageAttachment) mutAttrMemoText.append(imageAttributedString) //画像を追加したAttributedStringをmemoTextViewに追加 memoTextView.attributedText = mutAttrMemoText } //imagePickerを閉じる dismiss(animated: true, completion: nil) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true, completion: nil) } }・画像の圧縮やサイズ調整に必要な値:パラメーターとか、別の言い方が良さそう
・圧縮した画像:変数名でわかるし、圧縮率を示した方がいい
・インスタンスの宣言:何を宣言したのかを書くべき
・memoTextViewのテキストをAttributedStringに変換:テキストをメモに変えて、記号や英語で示した方が分かりやすそう
・画像をNSAttributedStringに変換:画像の詳細がないと、ImagePickerの画像なのか圧縮した画像なのかがわからない
・画像を追加したAttributedStringをmemoTextViewに追加:短く言い換えられそう
DisplayMemo
//ViewControllerから値を受け取る変数群 var selectedMemoObject = MemoModel() var selectedMemo_attributedText = NSAttributedString() var selectedIndexPathRow = Int() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) //前画面で選択されたメモのテキストを表示 memoTextView.attributedText = selectedMemo_attributedText } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "edit" { //EditMemoのインスタンス let vc = segue.destination as! EditMemo //EditMemoのプロパティに代入 vc.selectedMemoObject = selectedMemoObject vc.selectedMemo_attributedText = memoTextView.attributedText vc.selectedIndexPathRow = selectedIndexPathRow } }・ViewControllerから値を受け取る変数群:ViewControllerで選択された、の方が分かりやすい
・imagePickerの設定:無くてもわかる
・前画面で選択されたメモのテキストを表示:変数の宣言領域でViewControllerから受け取ったとわかれば、コメントの必要がない
EditMemo
@IBAction func updateMemo(_ sender: Any) { //編集したデータ let data2 = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false) //Realm内にある編集した元データの指定先 let memo = realm.objects(MemoModel.self).filter("identifier == %@", selectedMemoObject.identifier!) //Realmに書き込み try! realm.write { memo.setValue(data2, forKey: "data") } //戻る self.navigationController?.popToRootViewController(animated: true) }・編集したデータ:EditMemoの画面なので、編集したと書かなくても、データを編集していることは知っている
・Realm内にある編集した元データの指定先:Referenceの方がすっきりしてる
・Realmに書き込み:Updateの方が分かりやすい
extension UIImage { //データサイズを変更する func resizeImage(withPercentage percentage: CGFloat) -> UIImage? { //指定したパーセンテージの割合で画像を拡大/縮小する let canvas = CGSize(width: size.width * percentage, height: size.height * percentage) //リサイズ後の画像を返す return UIGraphicsImageRenderer(size: canvas, format: imageRendererFormat).image { _ in draw(in: CGRect(origin: .zero, size: canvas)) } } }・データサイズを変更する:関数名でリサイズってわかるし、無くても良いかも
・指定したパーセンテージの割合で画像を拡大/縮小する:リサイズ用のCGSizeなので、サイズ情報とかの方が分かりやすい
・リサイズ後の画像を返す:返り値がUIImageなので、コメントが無くても、リサイズした画像を返すって理解できる
変なコメントを改善する
ViewController
// Realmへ保存用のメモリスト var memoListForRealm:Results<MemoModel>! // Realm let realm = try! Realm() // メモリスト var memoList = [NSAttributedString]() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) memoList = [NSAttributedString]() // Realm-Read memoListForRealm = realm.objects(MemoModel.self) // memoList[i]: memoListForRealm.data -> NSAttributedString // memoList[i] add to attributedTextArray if memoListForRealm.count > 0 { for i in 0...memoListForRealm.count-1 { let attributeText = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(memoListForRealm![i].data) as! NSAttributedString memoList.append(attributeText) } } memoTableView.reloadData() }・Realmへ保存用のメモリスト:複数のメモを持つだけでなく、Realmに保存するのが役割
・Realm:Realmのインスタンスは他に無く、Realmの処理の本体であることを明示
・メモリスト:本プロジェクト内で共通の呼び名として、メモリスト、とコメント
・Realm-Read:RealmからデータをRead(取得)することを簡単に示す
・memoList[i]: memoListForRealm.data -> NSAttributedString():型の変換を矢印で、()で型(クラス)だと示す
・memoList[i] add to attributedTextArray:配列に追加するのを記号で表す方法を思いつけなかったので、英語でコメント
・memoTableView.reloadData()で、テーブルビューの更新だとわかるので、コメントは削除
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { let selectedMemo = memoListForRealm[indexPath.row] //Realm-Delete try! realm.write() { realm.delete(selectedMemo) } tableView.reloadData() } /***/ override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "display" { // DisplayMemoに選択したメモを渡す let vc = segue.destination as! DisplayMemo vc.selectedMemoObject = memoListForRealm[memoTableView.indexPathForSelectedRow!.row] vc.selectedMemoString = memoList[memoTableView.indexPathForSelectedRow!.row] vc.selectedIndexPathRow = memoTableView.indexPathForSelectedRow!.row } }・変数名で選択されたメモだとわかるので、コメントは削除
・Realm-Delete:Realmで削除を行うことを示す
・DisplayMemoに選択したメモを渡す:値渡しの処理をまとめて示す
AddMemo
// Model for Realm class MemoModel: Object { @objc dynamic var data: Data! @objc dynamic var identifier: String! }・Model for Realm:Realm用のモデルであることを端的に明示
override func viewDidLoad() { super.viewDidLoad() imagePicker.delegate = self imagePicker.sourceType = .photoLibrary }・imagePickerは1つの変数でのみ使用し、設定する処理も多くないので、コメントは抜いた
@IBAction func addMemo(_ sender: Any) { let memoObject = MemoModel() // memoTextView.attributedText -> Data() let attributedMemoData = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false) memoObject.data = attributedMemoData memoObject.identifier = String().randomString() // Realm-Add try! realm.write{ realm.add(memoObject) } // Back to ViewController self.navigationController?.popViewController(animated: true) }・memoTextView.attributedText -> Data():プロパティ名も記載し、Data型への変換を示す
・Realm-Add:Realmへのデータ追加処理を示す
・Back to ViewController:頭文字を大にすることでクラスやオブジェクトだと示し、Back to、で〜へ戻ることを示す
// 長押しタップ -> アラート表示 -> ImagePicker起動 @IBAction func attachImageGesture(_ sender: UILongPressGestureRecognizer) { let alert = UIAlertController(title: "画像を添付", message: nil, preferredStyle: .actionSheet) let okAction = UIAlertAction(title: "OK", style: .default) { (action) in self.present(self.imagePicker, animated: true, completion: nil) } let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil) alert.addAction(okAction) alert.addAction(cancelAction) present(alert, animated: true, completion: nil) }・長押しタップ -> アラート表示 -> ImagePicker起動:矢印で複数の異なる処理を順序付きで示す。(異なる処理に個別にコメントをつけてもよかったが、簡潔に少ないコメントの方がすっきりして見やすい)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let pickerImage = info[.originalImage] as? UIImage { // NSAttributedString用のパラメーター let width = pickerImage.size.width let padding = self.view.frame.width / 2 let scaleRate = width / (memoTextView.frame.size.width - padding) // 10%に圧縮した画像 let resizedImage = pickerImage.resizeImage(withPercentage: 0.1)! let imageAttachment = NSTextAttachment() var imageAttributedString = NSAttributedString() // memoTextView.attributedText -> NSMutableAttributedString let mutAttrMemoString = NSMutableAttributedString(attributedString: memoTextView.attributedText) // resizedImage -> NSAttributedString imageAttachment.image = UIImage(cgImage: resizedImage.cgImage!, scale: scaleRate, orientation: resizedImage.imageOrientation) imageAttributedString = NSAttributedString(attachment: imageAttachment) mutAttrMemoString.append(imageAttributedString) // 画像を追加後のテキスト -> memoTextView.attributedText memoTextView.attributedText = mutAttrMemoString } dismiss(animated: true, completion: nil) }・NSAttributedString用のパラメーター:用途を記載し、パラメーターで単位や数値の役割であることを示す
・10%に圧縮した画像:実際の圧縮例で処理の中身を知れる
・resizedImage -> NSAttributedString():変数resizedImageを変換する過程を示す
・画像を追加後のテキスト -> memoTextView.attributedText:最新のテキスト、と記載するより、どのようなテキスト、かを示した方が理解しやすい
DisplayMemo
// ViewControllerから値を受け取る var selectedMemoObject = MemoModel() var selectedMemoString = NSAttributedString() var selectedIndexPathRow = Int() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) memoTextView.attributedText = selectedMemoString } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "edit" { // EditMemoに保持中のメモを渡す let vc = segue.destination as! EditMemo vc.selectedMemoObject = selectedMemoObject vc.selectedMemoString = selectedMemoString vc.selectedIndexPathRow = selectedIndexPathRow } }・ViewControllerから値を受け取る:受け取り元を変数名に含めてもよかったが、各変数に含めると名前が長くなるので、コメントの方に含めた
EditMemo
@IBAction func updateMemo(_ sender: Any) { // memoTextView.attributedText -> Data() let editedMemoTextData = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false) // Realm内のデータを参照 let realmRef = realm.objects(MemoModel.self).filter("identifier == %@", selectedMemoObject.identifier!) // Realm-Update try! realm.write { realmRef.setValue(editedMemoTextData, forKey: "data") } // to ViewContrller self.navigationController?.popToRootViewController(animated: true) }・Realm内のデータを参照:何のデータかを示すこともできたが、コメントが長くなりそうなので、参照を表すことを記述
・Realm-Update:Realm内にあるデータを更新するので、Updateと記述
extension UIImage { func resizeImage(withPercentage percentage: CGFloat) -> UIImage? { // 圧縮後のサイズ情報 let canvas = CGSize(width: size.width * percentage, height: size.height * percentage) // return resized Image return UIGraphicsImageRenderer(size: canvas, format: imageRendererFormat).image { _ in draw(in: CGRect(origin: .zero, size: canvas)) } } }・圧縮後のサイズ情報:引数の圧縮率で算出されたCGSize(幅と高さ)だが、わかりやすく、画像のサイズに関する変数だと記述
・return resized Image:関数名と返り値の型で返り値の中身がわかるが、ぱっと見でわかるように、英語で記述
まとめ
今回、コメントを改善する中で、一部の変数名やインデントを修正しました。
コメントは文章や複数の記号を用いて複雑な説明を伝えることができますが、変数名よりも多くの情報を詰め込めるので、中途半端なコメントじゃ意味がありません。
変数名では伝えきれない情報を伝える、それがコメントの役割だと気付きました。
つまり、変数名とコメント、それぞれがお互いの情報伝達の欠点を補っているのです。
もちろん、インデントやコードのロジックも、情報を効率よく伝える要素の一つです。
ただ、変数名とコメントは、自由な形で情報を伝えられるので、時間をかけてでも取り組むべきだなと思いました。
次回は、コードのロジック、を改善していきます。
- 投稿日:2020-08-30T09:12:46+09:00
[Xcode] [Swift] 実務的Tips Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Unwind segue
概要・使い所
- 2つ以上前の画面に一気に戻りたい時。
- 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。
手順
- FirstViewController > SecondViewController はpush遷移です。
- SecondViewController > ThirdViewController はmodal遷移です。
やりたいことは「ThirdViewControllerからFirstViewControllerに一気に戻りたい」です。
手順1.戻り先に関数を定義する
戻り先であるFirstViewControllerに以下のようなコードを書きます。
関数名はなんでも良いです。FirstViewController.swift@IBAction func backToFirst(_ segue: UIStoryboardSegue) { print("\(segue.identifier!)") }手順2.遷移元にUnwind Segueを定義する
Storyboardの遷移元のViewControllerの"Exit"に対して、Ctrlを押しながらオブジェクトをドラッグするとUnwind Segueを定義できます。
例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義した
backToFirst
が表示されますので、それを選択するとUnwind Segueを追加できます。
Unwind Segueを追加したら任意のSegue Identifierを付けます。
実行結果
ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。
また、FirstViewControllerの
backToFirst(:_)
が実行され、segue.identifier
に上で付けたfromThirdToFirst
が渡ってきます。
すなわち、backToFirst(:_)
に、画面更新など、戻り先で行うべきコードを記述することができます。蛇足
私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。
- 投稿日:2020-08-30T09:12:46+09:00
[Xcode] [Swift] 実務的Tips: Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Unwind segue
概要・使い所
- 2つ以上前の画面に一気に戻りたい時。
- 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。
手順
- FirstViewController > SecondViewController はpush遷移です。
- SecondViewController > ThirdViewController はmodal遷移です。
やりたいことは「ThirdViewControllerからFirstViewControllerに一気に戻りたい」です。
手順1.戻り先に関数を定義する
戻り先であるFirstViewControllerに以下のようなコードを書きます。
関数名はなんでも良いです。FirstViewController.swift@IBAction func backToFirst(_ segue: UIStoryboardSegue) { print("\(segue.identifier!)") }手順2.遷移元にUnwind Segueを定義する
Storyboardの遷移元のViewControllerの"Exit"に対して、Ctrlを押しながらオブジェクトをドラッグするとUnwind Segueを定義できます。
例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義した
backToFirst
が表示されますので、それを選択するとUnwind Segueを追加できます。
Unwind Segueを追加したら任意のSegue Identifierを付けます。
実行結果
ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。
また、FirstViewControllerの
backToFirst(:_)
が実行され、segue.identifier
に上で付けたfromThirdToFirst
が渡ってきます。
すなわち、backToFirst(:_)
に、画面更新など、戻り先で行うべきコードを記述することができます。蛇足
私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。
- 投稿日:2020-08-30T09:12:46+09:00
[Xcode] [Swift] 今日からすぐ使える実務的Tipsシリーズ(2) Unwind Segue
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Unwind segue
概要・使い所
- 2つ以上前の画面に一気に戻りたい時。
- 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。
手順
- FirstViewController > SecondViewController はpush遷移です。
- SecondViewController > ThirdViewController はmodal遷移です。
やりたいことは「ThirdViewControllerからFirstViewControllerに一気に戻りたい」です。
手順1.戻り先に関数を定義する
戻り先であるFirstViewControllerに以下のようなコードを書きます。
関数名はなんでも良いです。FirstViewController.swift@IBAction func backToFirst(_ segue: UIStoryboardSegue) { print("\(segue.identifier!)") }手順2.遷移元にUnwind Segueを定義する
Storyboardの遷移元のViewControllerの"Exit"に対して、Ctrlを押しながらオブジェクトをドラッグするとUnwind Segueを定義できます。
例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義した
backToFirst
が表示されますので、それを選択するとUnwind Segueを追加できます。
Unwind Segueを追加したら任意のSegue Identifierを付けます。
実行結果
ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。
また、FirstViewControllerの
backToFirst(:_)
が実行され、segue.identifier
に上で付けたfromThirdToFirst
が渡ってきます。
すなわち、backToFirst(:_)
に、画面更新など、戻り先で行うべきコードを記述することができます。蛇足
私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。
- 投稿日:2020-08-30T09:12:46+09:00
[Xcode] [Swift] 実務的Tipsシリーズ(2) Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Unwind segue
概要・使い所
- 2つ以上前の画面に一気に戻りたい時。
- 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。
手順
- FirstViewController > SecondViewController はpush遷移です。
- SecondViewController > ThirdViewController はmodal遷移です。
やりたいことは「ThirdViewControllerからFirstViewControllerに一気に戻りたい」です。
手順1.戻り先に関数を定義する
戻り先であるFirstViewControllerに以下のようなコードを書きます。
関数名はなんでも良いです。FirstViewController.swift@IBAction func backToFirst(_ segue: UIStoryboardSegue) { print("\(segue.identifier!)") }手順2.遷移元にUnwind Segueを定義する
Storyboardの遷移元のViewControllerの"Exit"に対して、Ctrlを押しながらオブジェクトをドラッグするとUnwind Segueを定義できます。
例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義した
backToFirst
が表示されますので、それを選択するとUnwind Segueを追加できます。
Unwind Segueを追加したら任意のSegue Identifierを付けます。
実行結果
ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。
また、FirstViewControllerの
backToFirst(:_)
が実行され、segue.identifier
に上で付けたfromThirdToFirst
が渡ってきます。
すなわち、backToFirst(:_)
に、画面更新など、戻り先で行うべきコードを記述することができます。蛇足
私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。
- 投稿日:2020-08-30T09:09:52+09:00
[Swift] 実務的Tips Initialization Closureを使ってオブジェクトの初期化コードをリーダブルにする
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Initialization Closure
概要・使い所
プロパティが属する型が初期化されるたびに、クロージャーが呼び出され、その戻り値がプロパティのデフォルト値として割り当てられます。
特に、UIをコードで構築する場合に効果を発揮します。
プロパティの宣言部分に初期化のためのコードを集約できるため、見通しが良くなります。class ViewController: UIViewController { private let label: UILabel = { let l = UILabel() l.translatesAutoresizingMaskIntoConstraints = false l.text = "label" return l }() private let button: UIButton = { let b = UIButton() b.translatesAutoresizingMaskIntoConstraints = false b.setTitle("button", for: .normal) b.setTitleColor(.blue, for: .normal) b.sizeToFit() return b }() // 以下省略 }公式ドキュメント
- 投稿日:2020-08-30T09:09:52+09:00
[Swift] 実務的Tips: Initialization Closureを使ってオブジェクトの初期化コードをリーダブルにする
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Initialization Closure
概要・使い所
プロパティが属する型が初期化されるたびに、クロージャーが呼び出され、その戻り値がプロパティのデフォルト値として割り当てられます。
特に、UIをコードで構築する場合に効果を発揮します。
プロパティの宣言部分に初期化のためのコードを集約できるため、見通しが良くなります。class ViewController: UIViewController { private let label: UILabel = { let l = UILabel() l.translatesAutoresizingMaskIntoConstraints = false l.text = "label" return l }() private let button: UIButton = { let b = UIButton() b.translatesAutoresizingMaskIntoConstraints = false b.setTitle("button", for: .normal) b.setTitleColor(.blue, for: .normal) b.sizeToFit() return b }() // 以下省略 }公式ドキュメント
- 投稿日:2020-08-30T09:09:52+09:00
[Swift] 今日からすぐ使える実務的Tipsシリーズ(1) Initialization Closure
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Initialization Closure
概要・使い所
プロパティが属する型が初期化されるたびに、クロージャーが呼び出され、その戻り値がプロパティのデフォルト値として割り当てられます。
特に、UIをコードで構築する場合に効果を発揮します。
プロパティの宣言部分に初期化のためのコードを集約できるため、見通しが良くなります。class ViewController: UIViewController { private let label: UILabel = { let l = UILabel() l.translatesAutoresizingMaskIntoConstraints = false l.text = "label" return l }() private let button: UIButton = { let b = UIButton() b.translatesAutoresizingMaskIntoConstraints = false b.setTitle("button", for: .normal) b.setTitleColor(.blue, for: .normal) b.sizeToFit() return b }() // 以下省略 }公式ドキュメント
- 投稿日:2020-08-30T09:09:52+09:00
[Swift] 実務的Tipsシリーズ(1) Initialization Closureを使ってオブジェクトの初期化をカプセル化する
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Initialization Closure
概要・使い所
プロパティが属する型が初期化されるたびに、クロージャーが呼び出され、その戻り値がプロパティのデフォルト値として割り当てられます。
特に、UIをコードで構築する場合に効果を発揮します。
プロパティの宣言部分に初期化のためのコードを集約できるため、見通しが良くなります。class ViewController: UIViewController { private let label: UILabel = { let l = UILabel() l.translatesAutoresizingMaskIntoConstraints = false l.text = "label" return l }() private let button: UIButton = { let b = UIButton() b.translatesAutoresizingMaskIntoConstraints = false b.setTitle("button", for: .normal) b.setTitleColor(.blue, for: .normal) b.sizeToFit() return b }() // 以下省略 }公式ドキュメント
- 投稿日:2020-08-30T09:09:52+09:00
[Swift] 実務的Tipsシリーズ(1) Initialization Closureを使ってオブジェクトの初期化コードをリーダブルにする
今日からでもすぐに取り入れられて、
- コードをよりクリーンにできる、とか
- 工数を削減できる、とか
そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。前提環境
- Xcode 11.3.1
- Swift 5.1.3
Initialization Closure
概要・使い所
プロパティが属する型が初期化されるたびに、クロージャーが呼び出され、その戻り値がプロパティのデフォルト値として割り当てられます。
特に、UIをコードで構築する場合に効果を発揮します。
プロパティの宣言部分に初期化のためのコードを集約できるため、見通しが良くなります。class ViewController: UIViewController { private let label: UILabel = { let l = UILabel() l.translatesAutoresizingMaskIntoConstraints = false l.text = "label" return l }() private let button: UIButton = { let b = UIButton() b.translatesAutoresizingMaskIntoConstraints = false b.setTitle("button", for: .normal) b.setTitleColor(.blue, for: .normal) b.sizeToFit() return b }() // 以下省略 }公式ドキュメント
- 投稿日:2020-08-30T07:42:40+09:00
【備忘録】【iOS】SwiftエンジニアがObject-Cを学ぶ際に最低限学んだこと
最初にさらっと読んだ記事
今さらまとめるObjective-C文法基礎
→変数やif分などの基礎文法を学ぶ。
SwiftとObjective-Cとの違い
→クラスや関数などを学ぶ。
意図せずObjective-C を読まなければいけなくなった Swift エンジニアへ贈る Objective-C読み方まとめ
→SwiftとObjective-Cの違いで大事な点だけ学ぶ。上記でさらっとObjective-Cを理解。
後は不明点が出たらググるスタンスで良いのかなと思います。以下はメモ。
.hと.mのファイルが存在する
.hファイルとは
ヘッダファイル(.h)はクラスの「目次」
→このファイルのみでクラスの全体を俯瞰できる。
→@interface〜@end
→クラスの目次(宣言)(〜終わり).mファイルとは
実装ファイル(.m)はクラスの「本体」
→@implementation〜@end
→クラス実装(〜終わり)分割メリット
※分割のメリット・・クラスを外部に公開する際、ヘッダファイルだけでもユーザはそのクラスを利用できる。
一方実装の内部は隠蔽することができる。引用先:Objective-Cの基本
importの種類
#import
クラスライブラリ(CocoaやCocoa Touchに予め用意されているクラス)をインポートするときに使う
#import "hoge.h"
自作ファイルからインポートする場合
@import hoge;
Xcode5あたりで新規作成したprojectからデフォルトでYESになっている機能だったようです。
この機能を利用するためには、BuildSettingsからEnable Modules (C and Objective-C)をYESにする必要があります。
(※古いプロジェクトでは、デフォルトがNOになっている)#import <Cocoa/Cocoa.h>などのように書いていた箇所は
@import Cocoa;で置き換えられます。引用先:frameworkのimportを簡略化する(@importについて)
おわりに
後はググりながらObjective-Cを学んでいこうと思います。
躓いた点があれば随時追記してこうと思います!
- 投稿日:2020-08-30T02:53:09+09:00
[Swift] 条件を指定して配列を分割できる`partition(by:)`の使い方
partition(by:)
とは与えた条件に一致するすべての要素が、一致しないすべての要素の後に来るように、コレクションの要素を並べ替えます。
使い方
var numbers = Array(0...10) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let p = numbers.partition(by: { $0 % 2 == 0 }) // 5 print(numbers) // [9, 1, 7, 3, 5, 4, 6, 2, 8, 0, 10] let odds = numbers[..<p] // [9, 1, 7, 3, 5] let evens = numbers[p...] // [4, 6, 2, 8, 0, 10]注意点
partition(by:)
は条件に一致する要素が始まるindexを返すのであって、新しい配列を返すわけではない。
- つまり
partition(by:)
は自身を変化させるmutating func
(元の配列を宣言する時はlet
ではなくvar
)- 要素を並び替えた後、条件を満たさない前半(or満たす後半)の要素の連続の中で元の順番は保証されていない
問題点 & 対応策
前述したように、
partition(by:)
は条件によって配列を2つに分割することができますが、元の配列の順番が重要で、分割後もそれを維持したいケースにこのメソッドは向いていないです(そして多分そのケースの方が多い?)対応策として
Array
を拡張するextension Array { /// 条件を満たさない要素の配列と、満たす配列をtupleにして返す func partitioned(predicate: (Element) -> Bool) -> (unsatisfied: [Element], satisfied: [Element]) { return reduce(into: ([Element](), [Element]())) { if predicate($1) { $0.1.append($1) } else { $0.0.append($1) } } } }使い方
let partitioned = numbers.partitioned(predicate: { $0 % 2 == 0}) print(partitioned.unsatisfied) // [1, 3, 5, 7, 9] print(partitioned.satisfied) // [0, 2, 4, 6, 8, 10] // 順番はそのまま!ちなみに、今回extensionに利用した
reduce(into:_:)
については、こちらに素晴らしい記事があります。また、パフォーマンスについては
reduce(into:)
ではなく素直にfor in
を使って回す方法もありますが、リリースビルドにおいては問題無いようです。
(あっても自分はreduceで書きたいが?)最後に
当然ですが、分割した配列内の要素の順番を気にしないのであれば、パフォーマンスの観点からSwift標準の
partition(by:)
を使うべきですね?