【アプリ開発・トラブルシューティング】Swiftで使える便利な技【25選】

CrashlyticsのdSYMを登録したい

FirebaseのCrashlyticsにて「不足している必須のdSYMをアップロードしてください」と表示される問題の解決方法。

  1. オーガナイザーからArchiveしたパッケージをFinderで表示
  2. xcarchiveファイルを「Show Package Contents」で中身を覗き、dSYMsフォルダをターミナルに貼り付けるなどして、パス /path/dSYMs を取得する
  3. たとえば HogeProj があった場合、プロジェクトのディレクトリへ移動して以下のコマンドを実行する
./Pods/FirebaseCrashlytics/upload-symbols -gsp HogeProj/GoogleService-Info.plist -p ios /path/dSYMs

保存したファイルをファイル.appに表示させたい

何かのファイルを FileManager を使って documentDirectory に保存しても、デフォルトでは ファイル.app で表示されない。

以下の設定をInfo.plistに追加することで、ファイル.app に表示される。

UIFileSharingEnabled (Application supports iTunes file sharing)LSSupportsOpeningDocumentsInPlace (Supports opening documents in place) をそれぞれ YES に設定する。


画像の拡大


画像の拡大

UIBarButtonItemの画像サイズが大きくなってしまう


画像の拡大

次のプログラムで UIBarButtonItem を作ってしまうと、この画像のようにボタンのイメージが大きくなり崩れてしまうことがある。

let setting = UIBarButtonItem(image: UIImage(named: "gear"), style: .plain, target: self, action: #selector(clickedSettingButton(_:)))
self.navigationItem.leftBarButtonItems = [setting]

少し面倒だが、この問題を修正するには customViewUIButton を入れて、AutoLayout を設定することで解決できる。

let settingBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
settingBtn.addTarget(self, action: #selector(clickedSettingButton(_:)), for: .touchUpInside)
settingBtn.setImage(UIImage(named: "gear"), for: .normal)
let setting = UIBarButtonItem(customView: settingBtn)
setting.customView?.widthAnchor.constraint(equalToConstant: 24.0).isActive = true
setting.customView?.heightAnchor.constraint(equalToConstant: 24.0).isActive = true

self.navigationItem.leftBarButtonItems = [setting]

キレイにボタンをおさめることができた。


画像の拡大

キーボードを表示した時、Viewが隠れないようにしたい

IBOutletNSLayoutConstraint を図のように、特定のViewのボトムラインに結びつける。


画像の拡大

IBOutlet で結び付けられた NSLayoutConstraint は、コードで簡単に値を変えることができるようになった。下のプログラムでは、キーボードの高さと、表示アニメーションのdurationを取得して、UIView.animate でキーボードと同じ動きでViewが移動するようにアニメーションをかけている。

class ViewController: UIViewController {

    @IBOutlet weak var bottomLayoutConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
    }


    
    @objc func keyboardWillShow(notification: NSNotification) {
        guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
            return
        }
        guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {
            return
        }


        self.bottomLayoutConstraint.constant = -keyboardSize.height
        
        UIView.animate(withDuration: duration, animations: { () -> Void in
            self.view.layoutIfNeeded()
        })
    }
    
    @objc func keyboardWillHide(notification: NSNotification) {

        guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {
            return
        }
        self.bottomLayoutConstraint.constant = 0
        
        UIView.animate(withDuration: duration, animations: { () -> Void in
            self.view.layoutIfNeeded()
        })
    }
}

このプログラムではオブザーバーを削除していないため、注意しておく。

Safe Areaのマージンを取得したい

iOS11から利用できる self.safeAreaInsets を使って取得できる。次のようにViewにextensionを定義しておくと便利。使う時は self.view.csSafeAreaInsets.top のようにできる。

extension UIView {
    var csSafeAreaInsets: UIEdgeInsets {
        if #available(iOS 11.0, *) {
            return self.safeAreaInsets
        } else {
            return .zero
        }
    }
}

画面の回転イベントを検知したい

画面回転時のイベントをフックするには UIDevice.orientationDidChangeNotification をオブザーバー登録する必要がある。向きが変わった時だけ処理をさせたい場合は、下のプログラムのように lastOrientation などで以前の状態を保持しておく工夫が必要である。

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
}

var lastOrientation: UIDeviceOrientation?

@objc func orientationDidChange(_ notification: NSNotification) {
    let device = UIDevice.current
    if device.orientation.isLandscape {
        if lastOrientation != nil && !lastOrientation!.isLandscape {
            print("横向きに変わった")
        }

    } else if device.orientation.isPortrait {
        if lastOrientation != nil && !lastOrientation!.isPortrait {
            print("縦向きに変わった")
        }
    }
    lastOrientation = device.orientation
}

UIViewControllerのクラス名を取得したい

次のように UIViewController を拡張しておくことで、他のクラスで UIViewController が渡された時にクラス名で条件分岐できるようになる。

extension UIViewController {
  var className: String {
      return String(describing: type(of: self))
  }
}

class hoge {
  func doSomething(rootViewController:UIViewController)
  
      let className = rootViewController.className
  
      switch className {
      case "HogeViewController":
          print("From HogeViewController")
          break
      case "FugaViewController":
          print("From FugaViewController")
          break
      default:
          break
      }
  }
}

同じクラスタイプのViewを一括で操作したい

たとえば、UISegmentedControlがself.viewの中にたくさんあったとして、それらをまとめて処理したい場合は次のように書くことができる。

for v in view.subviews {
    if v.isKind(of: UISegmentedControl.self) {
        let sc = v as! UISegmentedControl
        if #available(iOS 13.0, *) {
            sc.selectedSegmentTintColor = .pink
        } else {
            // Fallback on earlier versions
        }
        sc.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], for: .normal)
        sc.backgroundColor = .black
    }
}

UIImageViewやUIButtonをtintColorで色を変えたい

通常、UIImageViewやUIButtonの画像の色は、tintColorを指定しても変えることができない。UIImage.RenderingMode.alwaysTemplateで画像をα値だけでレンダリングするように設定してあげればtintColorが効くようになる。

let image = UIImage(named: "hoge")?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate)
button.setImage(image, for: .normal)
button.tintColor = .red

ナビゲーションバーの色を変えたい

ナビゲーションバーの色は、それぞれ次のように定義されている。


画像の拡大

文字の色は、NSAttributedString を使って設定する。

// ナビゲーションバーのタイトル
self.navigationItem.title = "ランキング"

// ナビゲーションバーのタイトル色
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]

// ナビゲーションバーの背景色
self.navigationController?.navigationBar.barTintColor = .questionAreaBackground

let closeItem = UIBarButtonItem(title: "x", style: .plain, target: self, action: #selector(closeWindow))
// ボタンアイテムの色
closeItem.setTitleTextAttributes([
                                    NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 24),
                                    NSAttributedString.Key.foregroundColor: UIColor.white],
    for: .normal)
self.navigationItem.rightBarButtonItem = closeItem

ちなみに UIBarButtonItemtintColor でも変ることができる。

closeItem.tintColor = .white

また、ナビゲーションバーの色が少し薄い場合は、次のようにして対処する。

self.navigationController?.navigationBar.isTranslucent = false

テーブルセルの背景色を変えたい

UITableViewCell の背景色は、次のようにUITableViewのdelegateで変える。

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cell.backgroundColor = .black
}

MD5ハッシュ値の生成したい

CommonCrypto モジュールをインポートする必要がある。Stringextension しているので、使う時は "hogehoge".md5 のようにして変換することができる。

import Foundation
import CommonCrypto

enum CryptoAlgorithm {
    case MD5, SHA1, SHA224, SHA256, SHA384, SHA512
    
    var digestLength: Int {
        var result: Int32 = 0
        switch self {
        case .MD5:      result = CC_MD5_DIGEST_LENGTH
        case .SHA1:     result = CC_SHA1_DIGEST_LENGTH
        case .SHA224:   result = CC_SHA224_DIGEST_LENGTH
        case .SHA256:   result = CC_SHA256_DIGEST_LENGTH
        case .SHA384:   result = CC_SHA384_DIGEST_LENGTH
        case .SHA512:   result = CC_SHA512_DIGEST_LENGTH
        }
        return Int(result)
    }
}
extension String {
    var md5:    String { return digest(string: self, algorithm: .MD5) }
    var sha1:   String { return digest(string: self, algorithm: .SHA1) }
    var sha224: String { return digest(string: self, algorithm: .SHA224) }
    var sha256: String { return digest(string: self, algorithm: .SHA256) }
    var sha384: String { return digest(string: self, algorithm: .SHA384) }
    var sha512: String { return digest(string: self, algorithm: .SHA512) }

    func digest(string: String, algorithm: CryptoAlgorithm) -> String {
        var result: [CUnsignedChar]
        let digestLength = Int(algorithm.digestLength)
        if let cdata = string.cString(using: String.Encoding.utf8) {
            result = Array(repeating: 0, count: digestLength)
            switch algorithm {
            case .MD5:      CC_MD5(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA1:     CC_SHA1(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA224:   CC_SHA224(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA256:   CC_SHA256(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA384:   CC_SHA384(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA512:   CC_SHA512(cdata, CC_LONG(cdata.count-1), &result)
            }
        } else {
            fatalError("Nil returned when processing input strings as UTF8")
        }
        return (0..<digestLength).reduce("") { $0 + String(format: "%02hhx", result[$1])}
    }
}

参考

iOSの設定アプリへアクセスしたい

iOSの設定アプリの中の、自分のアプリの設定画面へショートカットできる。


画像の拡大

if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
   UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

数秒後に遅らせて実行させたい

DispatchQueue.main.asyncAfter を使えば、次のように遅延処理を簡単に書くことができる。

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  // 0.1秒後にしたい処理をここに書く
  ....
}

メインスレッドで実行したい

非同期処理の後にViewなどのUIを操作しようとするとクラッシュする。UIはメインスレッドで実行しなければならないからだ。DispatchQueue.main.async を使ってメインスレッド内で実行するようにしよう。

if Thread.isMainThread {
  print("MainThreadである")
} else {
  print("MainThreadではない")
}

DispatchQueue.main.async {
  // ここでviewなどを操作する
  ...
}

クロージャでコールバック処理したい

ネットワーク越しに非同期でデータを取ってきて、その後にViewなどにデータを反映したい場合によく使うワザ。completion(Int) -> Void でクロージャ定義する。ネットワーク処理などを書いて、成功したら completion(rank) で呼び出す。

func fetchYourRank(leaderboardId:String, completion: @escaping (Int) -> Void){
  
    let leaderBoard : GKLeaderboard = GKLeaderboard()

    ....
  
    leaderBoard.loadScores(completionHandler: {
        scores, error in
        if (error == nil) {
            let score = leaderBoard.localPlayerScore!
            let rank = score.rank

            completion(rank)
        }
    })
}

使う側はこんな感じ。引数にクロージャを定義することで分かりやすいコールバック処理を書くことができる。

fetchYourRank(leaderboardId: leaderboardId, completion: { rank in
    self.yourLankLabel.text = "あなたの順位は\(rank)位です"
})

画面の向きが変わったらイベントを検知したい

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
}

@objc func orientationDidChange(_ notification: NSNotification) {
    let device = UIDevice.current
    if device.orientation.isLandscape {
          print("横向き")

    } else if device.orientation.isPortrait {
          print("縦向き")
    }
}

UIDeviceOrientation には isFlatisValidInterfaceOrientation などもあることに注意。

端末がiPadかどうか判定したい

UIDevice.current.userInterfaceIdiomで判別できる。三項演算子と合わせるとスマートに書ける。

let h = (UIDevice.current.userInterfaceIdiom == .pad) ? 90 : 50

デバッグ時とリリース時で動作を変えたい

デバッグ時とリリース時で動作を変えるには、次のようにする。

#if DEBUG
    print("デバッグ時のみ実行")
#else
    print("リリース時のみ実行")
#endif

ただし、このDEBUGの定義を使えるようにするため、Builid SettingsOther Swift FlagsでDebugの項目のみに-D DEBUGと指定する必要がある。

必ずDebugの項目のみにパラメータを入れるようにしよう。


画像の拡大

シングルトンを実装したい

次のように static を使って宣言する。

使う時は Hoge.shared.doSomething() のように shared を経由して関数を呼び出す。init をオーバーライドして private にしておくと shared 忘れがなくてよい。

class Hoge {

    static let shared = Hoge()    

    private override init() {
        super.init()
    }

    func doSomething() {
        ...
    }
}

UIColorに独自カラーを追加したい

こんな感じでUIColorを拡張する。

extension UIColor {
    class var hogeColor: UIColor {
        return UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1)
    }
}

ポイントは関数ではなく変数として定義してclassを指定する。すると次のようにシステムカラーと同じような扱いで簡単に指定することができる。

textLabel.textColor = .textLightColor

UITalbeViewの最下部にスクロールしたい

最下部にスクロールするにはscrollToRowを使うが、tableView.reloadData() 後すぐに実行すると正しくスクロールできない。タイマーでタイミングを少しずらすことでうまくいった。

//tableViewに追加する処理をした後
tableView.reloadData()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    self.tableView.scrollToRow(at: IndexPath(row: self.items.count - 1, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
}

メソッドを一度だけ実行したい

viewDidLayoutSubviewsなど何度も呼び出されてしまうメソッド内で、一度だけ実行したい処理があった場合に以下の書き方をしていた。これだと毎回フラグを宣言、条件分岐するのは面倒だし、可読性がよくないなと思っていた。

var isFirst = false

override func viewDidLayoutSubviews() { // updateLayout
  if !isFirst {
     isFirst = true
    // 一度だけ実行したい処理を書く
  }  
}

クロージャーとlazyを使ったスマートな書き方を見つけたので、参考にさせていただいた。

private lazy var onceUpdateLayout: (()->())? = {
  // 一度だけ実行したい処理を書く
    return nil
}()

override func viewDidLayoutSubviews() { // updateLayout
  onceUpdateLayout?()
}

こちらの方が可読性は上がる。他にもいろいろな書き方がいろいろあるようだ。クロージャー(Closures)は名前のない関数で変数として扱われる。lazy(lazy stored property)は、参照されるときにはじめて初期値が設定されるプロパティ。

UILongPressGestureRecognizerは2回呼ばれる

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.longPressedButton))
button.addGestureRecognizer(longPressRecognizer)

こんな感じで登録したlongPressedButton関数は2回呼ばれる。アラートなど呼び出す時は二度呼ばれてしまうので注意が必要。UIGestureRecognizerのstateで下記のように切り分ける。

@objc func longPressedButton(sender:UIGestureRecognizer) {

    if (sender.state == .began){
      print("長押し開始のタイミング")      
      /*
      何かアラート処理を書いたり
      */
    } else if (sender.state == .ended) {
      print("長押し終了のタイミング")      
    }
}

メソッドチェーンの返り値を使わない時のwarningを消したい

メソッドチェーンを実装しようとした時に、返り値を使わない場合が多々ある。Xcodeは関数の返り値を使わないと丁寧にwarningを出してくれるが、warningだらけになってしまうので対処したかった。結論としては@discardableResultアノテーションを付ければよいだけ。

@discardableResult
func vibrate() -> Self {
    AudioServicesPlaySystemSound(1519)
    return self
}

Swiftオススメ書籍

Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus
Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

本書は、Swiftの言語仕様と実践的な利用方法を解説した入門書です。Swiftは簡潔な言語ですが、その言語仕様を理解し、正しく使うことはけっして容易ではありません。Appleの公式ドキュメントをはじめとして、どんな言語仕様があり、それらをどのように使うかに関しては豊富な情報源があります。しかし、それらがなぜ存在し、いつ使うべきかについてまとまった情報があるとは言えません。本書は、読者のみなさんの「なぜ」や「いつ」を解消することにも主眼を置いています。今回の改訂で、新バージョンのSwift 5に対応しました。

KindleAmazon

Amazonでお得に購入するなら、Amazonギフト券がオススメ!

\Amazonギフトがお得/

コンビニ・ATM・ネットバンキングで¥5,000以上チャージすると、プライム会員は最大2.5%ポイント、通常会員は最大2%ポイントがもらえます!
Amazonギフト券

\この記事をシェアする/