はじめに
iOS Advent Calendar 2020
17日目です。
2年半くらいiOSアプリ開発してきてハッとした瞬間をまとめました。(iOSとかswiftに限った話じゃない学びもあるけど。)
がんばらなくても読めるけど、なんとなく勉強にもなる記事を目指しました。
タイトルに近い方が初歩的なやつです。
もし時間あればみていただけると嬉しいです。
お品書き
- 返り値でBoolを返す時はそのBool自身を返せばいい
- 三項演算子を使うとif else がワンライナーで書ける
- var +=は計算型プロパティにできる。
- ネストは早期returnで減らせる
- 2重否定はifでいい。
- 型が明確な時のinitializerは.initに省略できる
- trailing closureは引数から省略できる
- enumとswitchを組み合わせて網羅性をチェックする
- Bool値が複数ある場合の場合分けはswitchとパターンマッチを使うと見やすくなる。
- var+forは大抵高階関数で書き換えられる
- 配列のmappingにはmapが使える
- for文にifを挟む処理はfor+whereに書き換えできる。
- for+whereはfilter+forEachに書き換えできる。
- 引数の変更を参照元に反映させたい時はinoutが使える
- filter + first or lastはfirst(where: ) or last(where: )で書ける。
- StackViewを使うと制約をグッと減らせる。
- StackView + ScrollViewでお手軽に拡張しやすいスクロール可能なレイアウトを構築できる。
- 見えているViewの裏のViewのタッチイベントを取得したい時はhittestが使える
- 同じ処理をアノテーション化して省略する時はPropertyWrapperが使える
返り値でBoolを返す時はそのBool自身を返せばいい
Before
var hoge: Bool {
if fugaBool {
return true
} else {
return false
}
}
After
var hoge: Bool {
return fugaBool
}
今思えば当たり前なんですけどね。
〇〇がtrueの時trueを返さなきゃってなって〇〇自体が真偽値なことに気づいてなかったんでしょうね。
三項演算子を使うと if else がワンライナーで書ける
Before
if fugaBool {
hogeLabel.text = "fugaは真です"
} else {
hogeLabel.text = "fugaは偽です"
}
After
hogeLabel.text = fugaBool ? "fugaは真です" : "fugaは偽です"
三項演算子すこ。
var +=
は計算型プロパティにできる。
Before
func presentHogeAlert() {
var message = ""
if validationA {
message.append("Aがあかんかった")
} else if validationB {
message.append("Bがあかんかった")
} else {
message.append("あかんかった")
}
let alert = UIAlertController(title: "エラー", message: message, preferredStyle: .alert)
alert.addAction(.init(title: "OK", style: .default))
present(alert, animated: true)
}
After
func presentHogeAlert() {
var message: String {
if validationA {
return "Aがあかんかった"
} else if validationB {
return "Bがあかんかった"
} else {
return "あかんかった"
}
}
let alert = UIAlertController(title: "エラー", message: message, preferredStyle: .alert)
alert.addAction(.init(title: "OK", style: .default))
present(alert, animated: true)
}
計算型の方が返り値がわかりやすくてスッキリしますよね。
ネストは早期returnで減らせる
Before
func hoge() {
if yagi {
// 処理A
if let mogu = mogu {
// 処理B
} else {
if fuga {
// 処理C
} else {
// 処理D
}
}
} else {
// 処理E
}
}
After
func hoge() {
guard yagi else {
// 処理E
return
}
// 処理A
guard let mogu = mogu else {
if fuga {
// 処理C
} else {
// 処理D
}
return
}
// 処理B
}
もうちょっといい例を用意したかったです。
ネストが多くて長いだけで読むのが辛くなっちゃいますよね。
2重否定はifでいい。
Before
guard !fugaBool else {
// do something
return
}
After
if fugaBool {
// do something
return
}
プロジェクトによってはguardで明示的にreturnを表現するためにあえて2重否定を好むところもあります。
真偽値一つならまだ分かるけどこれが!fugaBool || mogu > 1 && yagi.isEmpty
みたいな条件になってきたら発狂しますよね。
型が明確な時のinitializerは.init
に省略できる
Before
view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 0, height: 0))
After
view.frame = .init(
origin: .init(
x: 0,
y: 0
),
size: .init(
width: 0,
height: 0
)
)
長い型名を省略できるので好き。
多用すると読みにくくなるので注意。
trailing closure
は引数から省略できる
Before
func hogeChanged(fugaBool: Bool, hogeCompletionHandler: @escaping ((String) -> Void)) {
guard fugaBool else { return }
let hoge = getChangedHogeValue
hogeCompletionHandler(hoge)
}
hogeChanged(fugaBool: true, hogeCompletionHandler: @escaping { [weak self] hoge in
guard let self = self else { return }
self.hogeLabel.text = hoge
})
After
func hogeChanged(fugaBool: Bool, hogeCompletionHandler: @escaping ((String) -> Void)) {
guard fugaBool else { return }
let hoge = getChangedHogeValue
hogeCompletionHandler(hoge)
}
hogeChanged(fugaBool: true) { [weak self] hoge in
guard let self = self else { return }
self.hogeLabel.text = hoge
})
クロージャの引数名大抵長くなるから助かります。
読みづらくなる場合もあります。
enumとswitchを組み合わせて網羅性をチェックする
例えばTabBarControllerのtabの種類をenumにするとか。
Before
override func viewDidLoad() {
super.viewDidLoad()
var viewControllers: [UIViewController] = []
let hogeVC = HogeViewController())
hogeVC.tabBarItem = UITabBarItem(title: "hoge", image: nil, tag: 1)
viewControllers.append(hogeVC)
let fugaVC = FugaViewController()
fugaVC.tabBarItem = UITabBarItem(title: "fuga", image: nil, tag: 2)
viewControllers.append(fugaVC)
setViewControllers(viewControllers, animated: false)
}
After
enum TabType: Int, CaseIterable {
case hoge = 0
case fuga = 1
private var baseViewController: UIViewController {
switch self {
case .hoge:
return HogeViewController()
case .fuge:
return FugaViewController()
}
}
private var title: String {
switch self {
case .hoge:
return "hoge"
case .fuga:
return "fuga"
}
}
var tabItem: UITabBarItem {
.init(title: title, image: nil, tag: self.rawValue)
}
var viewController: UIViewController {
let viewController = baseViewController
viewController.tabBarItem = tabItem
return viewController
}
}
// 使う時
override func viewDidLoad() {
super.viewDidLoad()
setViewControllers(TabType.allCases.map(\.viewController), animated: false)
}
caseを追加すれば他の設定値も自ずと追加が必要になるので漏れを防げます。
Bool値が複数ある場合の場合分けはswitchとパターンマッチを使うと見やすくなる。
Before
フラグが何個かあってtrueとfalseの組み合わせを表現したい時。
let hogeFlag: Bool
let fugaFlag: Bool
if hogeFlag && fugaFlag {
// true true
} else if !hogeFlag && fugaFlag {
// false true
} else if hogeFlag && !fugaFlag {
// true flase
} else {
// false false
}
After
switch (hogeFlag, fugaFlag) {
case (true, true): break
case (false, true): break
case (true, false): break
case (false, false): break
}
switchの見やすさは異常。
var+for
は大抵高階関数で書き換えられる
Before
var titles: [String] = []
for i in 0...10 {
let title = "りんごが\(i)個あります。"
titles.append(title)
}
titles.joined(separator: "\n")
return titles
After
(0...10).map {
"りんごが\($0)個あります。"
}
.joined(separator: "\n")
https://qiita.com/shtnkgm/items/600009917d8e572e6780
こちらの記事が詳しいです。
配列のmappingにはmapが使える
Before
struct Hoge {
let aaa: String
let iii: String
let uuu: String
}
// Hogeの配列があります。
let hogeList = [Hoge]()
After
// 特定のプロパティのみ取り出し
let aaaList = hogeList.map(\.aaa)
// 別の型にmapping
struct Fuga {
let aaa: String
let eee: String
let ooo: String
}
let fugaList = hogeList.map { Fuga(aaa: $0.aaa, eee: "", ooo: "") }
API叩いてデータ加工してViewに渡す過程で絶対必要になりますよね。
for文にifを挟む処理はfor+where
に書き換えできる。
こういう動物達がいるとします。
protocol Animal {
var name: String { get }
}
protocol Runable: Animal {
func run()
}
extension Runable {
func run() {
print("run!!!")
}
}
class Cat: Animal, Runable {
var name: String = "猫"
}
class Dog: Animal, Runable {
var name: String = "犬"
}
class Penguin: Animal {
var name: String = "ペンギン"
}
let animals: [Animal] = [Cat(), Dog(), Penguin()]
Before
for animal in animals {
if animal is Runable {
print("この動物は走れます!")
}
if let runableAnimal = animal as? Runable {
runableAnimal.run()
}
}
After
for animal in animals where animal is Runable {
print("この動物は走れます!")
if let runableAnimal = animal as? Runable {
runableAnimal.run()
}
}
whereを使いこなしてるとなんかできる感出てきますよね。
for+where
はfilter+forEach
に書き換えできる。
先ほどの例から
Before
for animal in animals where animal is Runable {
print("\(animal.name)は走れます!")
if let runableAnimal = animal as? Runable {
runableAnimal.run()
}
}
After
animals.filter { $0 is Runable }.forEach {
print("\($0.name)は走れます!")
if let runableAnimal = $0 as? Runable {
runableAnimal.run()
}
}
高階関数すこ。
この例ならキャストしてcompactMapにかけた方が早いかも。
animals.compactMap { $0 as? Runable }.forEach {
print("\($0.name)は走れます!")
$0.run()
}
引数の変更を参照元に反映させたい時はinoutが使える
いわゆる参照渡しです。
Before
var fuga = "fugaです。"
print(fuga) // fugaです。
func addHogeString() {
fuga.append("\n")
fuga.append("hogeを追加します")
}
addHogeString()
print(fuga) // fugaです。\n hogeを追加します。
After
func addHogeString(strings: inout String) {
strings.append("\n")
strings.append("hogeを追加します")
}
var fuga = "fugaです。"
print(fuga) // fugaです。
addHogeString(strings: &fuga)
print(fuga) // fugaです。\n hogeを追加します。
この&を使った参照渡しの表現はCombineのassignでも使いますね。
filter + first or last
はfirst(where: ) or last(where: )
で書ける。
Before
animals.filter { !($0 is Runable) }.first // Penguin
After
animals.first { !($0 is Runable) } // Punguin
firstを使う方がパフォーマンスにも優れるとかなんとか。
StackViewを使うと制約をグッと減らせる。
こんなViewを用意したいとします。(枠が見えやすいように色を変えています。)
Before
class ViewController: UIViewController {
private lazy var hogeLabel: UILabel = {
let label = UILabel()
label.text = "hogehoge"
return label
}()
private lazy var fugaLabel: UILabel = {
let label = UILabel()
label.text = "fugafuga"
return label
}()
private lazy var moguLabel: UILabel = {
let label = UILabel()
label.text = "mogumogu"
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
}
private func setupLayout() {
[hogeLabel, fugaLabel, moguLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = .gray
view.addSubview($0)
}
NSLayoutConstraint.activate([
hogeLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 80),
hogeLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16),
hogeLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16),
fugaLabel.topAnchor.constraint(equalTo: hogeLabel.bottomAnchor, constant: 16),
fugaLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16),
fugaLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16),
moguLabel.topAnchor.constraint(equalTo: fugaLabel.bottomAnchor, constant: 16),
moguLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16),
moguLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16),
])
}
}
After
class ViewController: UIViewController {
...
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.spacing = 16
stackView.axis = .vertical
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
}
private func setupLayout() {
view.backgroundColor = .lightGray
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
[hogeLabel, fugaLabel, moguLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = .gray
stackView.addArrangedSubview($0)
}
NSLayoutConstraint.activate([
hogeLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 80),
hogeLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16),
hogeLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16),
])
}
}
StackViewすこ。使いすぎるとパフォーマンス落ちるとの声もあるので注意。
StackView + ScrollView
でお手軽に拡張しやすいスクロール可能なレイアウトを構築できる。
こんな感じにスクロールしたい画面を作る時はUIScrollViewを使いますが、ScrollViewはその中身のViewのサイズによってスクロールが発生するかどうかが決まるので中身をViewを用意してあげないといけません。そのViewをStackViewにしてやれば、stackViewにaddArreangedSubViewするだけでサイズが変わってくれるので、Viewの高さ計算とかしなくても楽にスクロールが発生します。
- ScrollViewをSuperViewの全方向に0pxで制約を貼る
- ScrollViewにStackViewをaddSubView
- StackViewとFrameLayoutGuideを同じ横幅にする
- StackViewをContentLayoutGuideの上下左右に0pxで制約を貼る
後はstackViewにViewを追加していくだけでStackViewのサイズが画面サイズより大きくなった時にスクロールが発生します。
スクロールなし | スクロールあり |
---|---|
見えているViewの裏のViewのタッチイベントを取得したい時はhittest
が使える
こういうScrollViewが重なった画面があるとします。
画像は今開発中の岩の位置を記録するアプリの画面です。
Before
そのままだと前面の縦ScrollViewがタッチイベントを吸収し、裏の横ScrollViewが反応しません。
After
そこでhittestです。
こいつでnilを返すようにoverrideすればタッチイベントを無視できます。
final class HeaderIgnorableScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self,
point.x < UIScreen.main.bounds.width && point.y < UIScreen.main.bounds.height * (9/16) {
return nil
}
return view
}
}
裏面の横ScrollViewもタッチできるようになりました。
感動しました。
同じ処理をアノテーション化して省略する時はPropertyWrapper
が使える
例えばKeychainへのsetとgetの処理。
Before
こんな感じでgetとsetをしてたとします。
final class KeychainManager {
/// キー
struct Key {
static let accessToken = "accessToken"
}
/// キーチェーンインスタンス
static let keychain = Keychain(service: Bundle.main.bundleIdentifier ?? "")
static var accessToken: String {
get { get(key: Key.accessToken) ?? "" }
set { set(key: Key.accessToken, value: newValue) }
}
private static func get<T>(key: String) -> T? {
do {
return try KeychainManager.keychain.get(key) as? T
} catch {
print(error.localizedDescription)
assertionFailure("Keychainからのデータの取得に失敗しました。")
return nil
}
}
private static func set(key: String, value: String) {
do {
return try KeychainManager.keychain.set(value, key: key)
} catch {
print(error.localizedDescription)
assertionFailure("Keychainへのデータの保存に失敗しました。。")
}
}
}
// 利用時
KeychainManager.accessToken = accessToken // set
let accessToken = KeychainManager.accessToken
After
propertyWrapperを使えばsetとかgetを書かなくてよくなります。
import KeychainAccess
@propertyWrapper
class KeychainStorage<T: LosslessStringConvertible> {
private let key: String
var keychain: Keychain {
guard let identifier = Bundle.main.object(forInfoDictionaryKey: UUID().uuidString) as? String else {
return Keychain(service: "")
}
return Keychain(service: identifier)
}
init(key: String) {
self.key = key
}
var wrappedValue: T? {
get {
do {
guard let result = try keychain.get(key) else { return nil }
return T(result)
} catch {
print(error.localizedDescription)
return nil
}
}
set {
do {
guard let new = newValue else {
try keychain.remove(key)
return
}
try keychain.set(String(new), key: key)
} catch {
print(error.localizedDescription)
}
}
}
}
final class KeychainDataHolder {
private enum Key: String {
case uid = "_accessToken"
}
static let shared: KeychainDataHolder = KeychainDataHolder()
private init() {}
@KeychainStorage(key: Key.accessToken.rawValue)
var accessToken: String?
}
// 利用時
KeychainDataHolder.shared.accessToken = accessToken // set
let accessToken = KeychainDataHolder.shared.accessToken //get
keyとプロパティを増やせばsetとgetは書かなくてよくなりますね。
終わりに
読んでいただいてありがとうございました!
もっといっぱいあると思うんですけどいざ出そうとすると出てこないんですよね。
明日は@takashicoさんです!