20200830のSwiftに関する記事は18件です。

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で続きを読む

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

Simulator Screen Shot - iPhone 11 - 2020-08-29 at 16.28.58.png

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

Simulator Screen Shot - iPhone 11 - 2020-08-29 at 16.30.32.png

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

完成!! 

Simulator Screen Shot - iPhone 11 - 2020-08-29 at 16.36.06.png

 参考

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

リーダブルコードで読みにくいコードを改善していく ~ コメント編

どうも、ねこきち(@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:関数名と返り値の型で返り値の中身がわかるが、ぱっと見でわかるように、英語で記述

まとめ

今回、コメントを改善する中で、一部の変数名やインデントを修正しました。

コメントは文章や複数の記号を用いて複雑な説明を伝えることができますが、変数名よりも多くの情報を詰め込めるので、中途半端なコメントじゃ意味がありません。

変数名では伝えきれない情報を伝える、それがコメントの役割だと気付きました。

つまり、変数名とコメント、それぞれがお互いの情報伝達の欠点を補っているのです。

もちろん、インデントやコードのロジックも、情報を効率よく伝える要素の一つです。

ただ、変数名とコメントは、自由な形で情報を伝えられるので、時間をかけてでも取り組むべきだなと思いました。

次回は、コードのロジック、を改善していきます。

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