115
87

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSAdvent Calendar 2020

Day 17

【これからiOS頑張りたい方向け】2年半iOSアプリ開発をしてハッとした瞬間まとめ

Last updated at Posted at 2020-12-16

はじめに

iOS Advent Calendar 2020 17日目です。:tada:

2年半くらいiOSアプリ開発してきてハッとした瞬間をまとめました。(iOSとかswiftに限った話じゃない学びもあるけど。)
がんばらなくても読めるけど、なんとなく勉強にもなる記事を目指しました。
タイトルに近い方が初歩的なやつです。
もし時間あればみていただけると嬉しいです。

お品書き

返り値で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+wherefilter+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 lastfirst(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さんです!

115
87
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
115
87

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?