20200830のiOSに関する記事は16件です。

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%B3

URL コンポーネント別の CharacterSet 一覧は下記になります。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

謎の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つとして、自分が直接書いたソースコードはスタック中にはなく、何をトリガに起きているのかわからないことがありました。
最終的にはUIStackViewinsertArrangedSubviewで落ちており、スタックの途中には_UIButtonBarStackViewなどが出ているので、多分UIBarButtonItemに関連する何かだとは思うのですが、どこで問題が起きているのか全くわかりませんでした。

発生の傾向を見ると、iOS13.4以降でしか起きていないようで、それがiOS13.4以降でしか発生しない現象なのか、たまたま時期的な関係でそう見えているのかはよくわかりませんでした。

原因と解決方法

結論としては、Apple Developers Forumにあった以下の記事が当たりでした。
https://developer.apple.com/forums/thread/130038

上記記事ではUIAlertControllerだそうですが、筆者のアプリでは画面下部のUIToolbarUIBarButtonItemを追加する際に、同じオブジェクトが含まれていると、iOS13.4以降ではクラッシュするという問題でした。(iOS13.3以前では同じコードでも問題は発生しない)

MyViewController.swift
self.setToolbarItems([filterButton, fixSpace1, dummyIcon, flexSpace1, filterStateButton,
     flexSpace2, dummyIcon, fixSpace2, dummyIcon], animated: true)

上記のソースコードが問題で、ツールバーにボタンを並べる際のレイアウト調整のために、見えないダミーのボタン(dummyIcon)を3つ追加していたのですが、その3つが同じオブジェクトであったためにクラッシュしていました。

以下のように3つ別のオブジェクトにすることで解決しました(オブジェクトの生成コード等は省略)

MyViewController.swift
self.setToolbarItems([filterButton, fixSpace1, dummyIcon1, flexSpace1, filterStateButton,
     flexSpace2, dummyIcon2, fixSpace2, dummyIcon3], animated: true)

手がかりがinsertArrangedSubviewくらいしかなくて難航したので、少しでもネット上で情報が見つかるように記事を書いてみました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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:)を行うとクラッシュする場合があります。
あまり発生しにくいケースだとは思いますが、ネット上にもほとんど情報が見つからなかったので、一応記事にしておきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

App Attest APIの概要

※2020/9/1執筆→11/16全体公開(忘れていました)

App Attest APIは、iOS 14以上で使用できる、DeviceCheckの一種です。

たとえばアプリがハックされると、チートや広告の削除、有料機能の不正利用などが行われます。
それを防ぐため、デバイスで特殊な暗号鍵を生成し、サーバーが機密データにアクセスする際にAppleの認証を受けることで、アプリが正当なことを証明できます。

App Attest API

アプリの実装方法

App Attest API

こちら参照。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")
    }
}

サーバーの実装方法

App Attest API

こちら参照。やることは以下

・(Your Appに対して)チャレンジの払い出しと保持
・(App attestに対して)認証オブジェクトの生成

※サーバーの実装方法はよくわかっておらず、調査中

DeviceCheckについて

App Attest単体では、不正を行ったデバイスの特定はできないので、特定したい場合DeviceCheckと併用します。
DeviceCheckは、こちらの記事が分かりやすいです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Xcode] [Swift] 実務的Tips Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する

今日からでもすぐに取り入れられて、

  • コードをよりクリーンにできる、とか
  • 工数を削減できる、とか

そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3

Unwind segue

概要・使い所

  • 2つ以上前の画面に一気に戻りたい時。
  • 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。

手順

例えば、こんな感じのStroyboardがあるとします。
スクリーンショット 2020-08-29 9.58.44.png

  • 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を定義できます。
スクリーンショット 2020-08-29 10.11.29.png

例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義したbackToFirstが表示されますので、それを選択するとUnwind Segueを追加できます。
スクリーンショット 2020-08-29 10.32.28.png

Unwind Segueを追加したら任意のSegue Identifierを付けます。
スクリーンショット 2020-08-29 10.33.58.png

実行結果

ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。

すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。

また、FirstViewControllerのbackToFirst(:_)が実行され、segue.identifierに上で付けたfromThirdToFirstが渡ってきます。
すなわち、backToFirst(:_)に、画面更新など、戻り先で行うべきコードを記述することができます。

蛇足

私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。:bow:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Xcode] [Swift] 実務的Tips: Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する

今日からでもすぐに取り入れられて、

  • コードをよりクリーンにできる、とか
  • 工数を削減できる、とか

そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3

Unwind segue

概要・使い所

  • 2つ以上前の画面に一気に戻りたい時。
  • 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。

手順

例えば、こんな感じのStroyboardがあるとします。
スクリーンショット 2020-08-29 9.58.44.png

  • 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を定義できます。
スクリーンショット 2020-08-29 10.11.29.png

例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義したbackToFirstが表示されますので、それを選択するとUnwind Segueを追加できます。
スクリーンショット 2020-08-29 10.32.28.png

Unwind Segueを追加したら任意のSegue Identifierを付けます。
スクリーンショット 2020-08-29 10.33.58.png

実行結果

ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。

すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。

また、FirstViewControllerのbackToFirst(:_)が実行され、segue.identifierに上で付けたfromThirdToFirstが渡ってきます。
すなわち、backToFirst(:_)に、画面更新など、戻り先で行うべきコードを記述することができます。

蛇足

私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。:bow:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Xcode] [Swift] 今日からすぐ使える実務的Tipsシリーズ(2) Unwind Segue

今日からでもすぐに取り入れられて、

  • コードをよりクリーンにできる、とか
  • 工数を削減できる、とか

そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3

Unwind segue

概要・使い所

  • 2つ以上前の画面に一気に戻りたい時。
  • 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。

手順

例えば、こんな感じのStroyboardがあるとします。
スクリーンショット 2020-08-29 9.58.44.png

  • 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を定義できます。
スクリーンショット 2020-08-29 10.11.29.png

例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義したbackToFirstが表示されますので、それを選択するとUnwind Segueを追加できます。
スクリーンショット 2020-08-29 10.32.28.png

Unwind Segueを追加したら任意のSegue Identifierを付けます。
スクリーンショット 2020-08-29 10.33.58.png

実行結果

ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。

すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。

また、FirstViewControllerのbackToFirst(:_)が実行され、segue.identifierに上で付けたfromThirdToFirstが渡ってきます。
すなわち、backToFirst(:_)に、画面更新など、戻り先で行うべきコードを記述することができます。

蛇足

私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。:bow:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Xcode] [Swift] 実務的Tipsシリーズ(2) Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する

今日からでもすぐに取り入れられて、

  • コードをよりクリーンにできる、とか
  • 工数を削減できる、とか

そんなTipsを紹介していく記事シリーズです。
「知らなかった」「気づかなかった」「忘れていた」そんな誰かの役に立てば幸いです。

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3

Unwind segue

概要・使い所

  • 2つ以上前の画面に一気に戻りたい時。
  • 特に、戻り先の画面にて、画面更新などの処理を行いたい時に便利。

手順

例えば、こんな感じのStroyboardがあるとします。
スクリーンショット 2020-08-29 9.58.44.png

  • 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を定義できます。
スクリーンショット 2020-08-29 10.11.29.png

例えば、UIButtonから"Exit"にCtrl+ドラッグするとこんなふうに上で定義したbackToFirstが表示されますので、それを選択するとUnwind Segueを追加できます。
スクリーンショット 2020-08-29 10.32.28.png

Unwind Segueを追加したら任意のSegue Identifierを付けます。
スクリーンショット 2020-08-29 10.33.58.png

実行結果

ThirdViewControllerのUIButtonをtapすると、
(1) ThirdViewControllerが閉じられ、
(2) SecondViewControllerからFirstViewControllerにpopされる、
という動きになります。

すなわち、「ThirdViewControllerからFirstViewControllerに一気に戻りたい」を簡単に実現できます。

また、FirstViewControllerのbackToFirst(:_)が実行され、segue.identifierに上で付けたfromThirdToFirstが渡ってきます。
すなわち、backToFirst(:_)に、画面更新など、戻り先で行うべきコードを記述することができます。

蛇足

私はコードでこれらを(Unwind Segueと同程度に簡単に)実現できる方法を知りません。
もしご存知の方がいたらコメントで教えていただきたいです。:bow:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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
    }()

// 以下省略
}

公式ドキュメント

The Swift Programming Language - Initialization

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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
    }()

// 以下省略
}

公式ドキュメント

The Swift Programming Language - Initialization

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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
    }()

// 以下省略
}

公式ドキュメント

The Swift Programming Language - Initialization

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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
    }()

// 以下省略
}

公式ドキュメント

The Swift Programming Language - Initialization

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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
    }()

// 以下省略
}

公式ドキュメント

The Swift Programming Language - Initialization

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】【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"

自作ファイルからインポートする場合

引用先:Objective-Cのimport

@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を学んでいこうと思います。
躓いた点があれば随時追記してこうと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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:)を使うべきですね?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む