Swiftで使える便利な小ワザ25選【iOSアプリ開発】
Swiftで使える便利な技をご紹介。実際にアプリ開発で役立つ小技を、忘備録的にまとめました。
- CrashlyticsのdSYMを登録したい
- 保存したファイルをファイル.appに表示させたい
- UIBarButtonItemの画像サイズが大きくなってしまう
- キーボードを表示した時、Viewが隠れないようにしたい
- Safe Areaのマージンを取得したい
- 画面の回転イベントを検知したい
- UIViewControllerのクラス名を取得したい
- 同じクラスタイプのViewを一括で操作したい
- UIImageViewやUIButtonをtintColorで色を変えたい
- ナビゲーションバーの色を変えたい
- テーブルセルの背景色を変えたい
- MD5ハッシュ値の生成したい
- iOSの設定アプリへアクセスしたい
- 数秒後に遅らせて実行させたい
- メインスレッドで実行したい
- クロージャでコールバック処理したい
- 画面の向きが変わったらイベントを検知したい
- 端末がiPadかどうか判定したい
- デバッグ時とリリース時で動作を変えたい
- シングルトンを実装したい
- UIColorに独自カラーを追加したい
- UITalbeViewの最下部にスクロールしたい
- メソッドを一度だけ実行したい
- UILongPressGestureRecognizerは2回呼ばれる
- メソッドチェーンの返り値を使わない時のwarningを消したい
CrashlyticsのdSYMを登録したい
FirebaseのCrashlyticsにて「不足している必須のdSYMをアップロードしてください」と表示される問題の解決方法。
- オーガナイザーからArchiveしたパッケージをFinderで表示
- xcarchiveファイルを「Show Package Contents」で中身を覗き、dSYMsフォルダをターミナルに貼り付けるなどして、パス /path/dSYMs を取得する
- たとえば 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]
少し面倒だが、この問題を修正するには customView に UIButton を入れて、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が隠れないようにしたい
IBOutlet で NSLayoutConstraint を図のように、特定の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
ちなみに UIBarButtonItem は tintColor でも変ることができる。
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 モジュールをインポートする必要がある。String を extension しているので、使う時は "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])}
}
}
参考 Swift4でハッシュ値の計算(MD5,SHA1,SHA256,etc.)
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("縦向き")
}
}
端末がiPadかどうか判定したい
UIDevice.current.userInterfaceIdiomで判別できる。三項演算子と合わせるとスマートに書ける。let h = (UIDevice.current.userInterfaceIdiom == .pad) ? 90 : 50
デバッグ時とリリース時で動作を変えたい
デバッグ時とリリース時で動作を変えるには、次のようにする。
#if DEBUG
print("デバッグ時のみ実行")
#else
print("リリース時のみ実行")
#endif
ただし、このDEBUGの定義を使えるようにするため、Builid SettingsのOther 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を使ったスマートな書き方を見つけたので、参考にさせていただいた。 UIView などでインスタンス単位で一度だけ実行したい初期化コードの考察 - Qiita
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
}