[SWIFT] [For those who want to do their best in iOS from now on] Summary of the moment when I was surprised after developing an iOS application for two and a half years

Introduction

iOS Advent Calendar 2020 It's the 17th day. : tada:

I've summarized the moments when I've been developing iOS apps for about two and a half years. (Although there is learning that is not limited to iOS or swift.) I aimed for an article that I could read without trying hard, but somehow it would be useful for studying. The one closer to the title is the rudimentary one. I would be grateful if you could take a look if you have time.

menu

When returning a Bool as a return value, just return the Bool itself.

Before

var hoge: Bool {
    if fugaBool {
        return true
    } else {
        return false
    }
}

After

var hoge: Bool {
    return fugaBool
}

It's natural when I think about it now. You probably didn't realize that XX itself is a boolean value because you had to return true when XX was true.

If else can be written in one liner using the ternary operator

Before

if fugaBool {
    hogeLabel.text = "fuga is true"
} else {
    hogeLabel.text = "fuga is fake"
}

After

hogeLabel.text = fugaBool ? "fuga is true" : "fuga is fake"

Ternary operator.

var + = can be a calculated property.

Before

func presentHogeAlert() {
    var message = ""
    if validationA {
        message.append("A was ok")
    } else if validationB {
        message.append("B was ok")
    } else {
        message.append("It was awesome")
    }
    let alert = UIAlertController(title: "error", 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 was ok"
        } else if validationB {
            return "B was ok"
        } else {
            return "It was awesome"
        }
    }

    let alert = UIAlertController(title: "error", message: message, preferredStyle: .alert)
    alert.addAction(.init(title: "OK", style: .default))
    present(alert, animated: true)
}

The calculation type makes the return value easier to understand, isn't it?

Nest can be reduced with early return

Before

func hoge() {
    if yagi {
        //Process A
        if let mogu = mogu {
            //Process B
        } else {
            if fuga {
                //Process C
            } else {
                //Process D
            }
        }
    } else {
        //Process E
    }
}

After

func hoge() {
    guard yagi else {
        //Process E
        return
    }
    
    //Process A

    guard let mogu = mogu else {
        if fuga {
            //Process C
        } else {
            //Process D
        }
        return 
    }

    //Process B
}

I wanted to prepare a better example. There are many nests and it's hard to read just because it's long.

Double negation can be if.

Before

guard !fugaBool else {
    // do something
    return
}

After

if fugaBool {
    // do something
    return
}

Some projects dare to prefer double negation in order to explicitly express return with guard. I still know if there is only one truth value, but this is!fugaBool || mogu > 1 && yagi.isEmptyIf the conditions are like this, you will go crazy.

The initializer when the type is clear can be omitted to .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
    )
)

I like it because I can omit long model names. Note that if you use it too much, it will be difficult to read.

trailing closure can be omitted from the argument

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

Closure argument names are usually long, which is helpful. It can be difficult to read.

Check completeness by combining enum and switch

For example, set the tab type of TabBarController to 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
    }
}

//When to use
override func viewDidLoad() {
    super.viewDidLoad()
    setViewControllers(TabType.allCases.map(\.viewController), animated: false)
}

If you add a case, you will need to add other setting values, so you can prevent leakage.

When there are multiple Bool values, it is easier to see if you use switch and pattern matching.

Before When there are several flags and you want to express a combination of true and 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
}

The visibility of the switch is abnormal.

var + for is usually rewritten by higher-order functions

Before

var titles: [String] = []
for i in 0...10 {
    let title = "Apples\(i)There are one."
    titles.append(title)
}
titles.joined(separator: "\n")
return titles

After

(0...10).map {
    "Apples\($0)There are one."
}
.joined(separator: "\n")

https://qiita.com/shtnkgm/items/600009917d8e572e6780 This article is detailed.

Map can be used for array mapping

Before

struct Hoge {
    let aaa: String
    let iii: String
    let uuu: String
}

//There is an array of Hoge.
let hogeList = [Hoge]()

After

//Extract only specific properties
let aaaList = hogeList.map(\.aaa)
//Mapping to another type
struct Fuga {
    let aaa: String
    let eee: String
    let ooo: String
}
let fugaList = hogeList.map { Fuga(aaa: $0.aaa, eee: "", ooo: "") }

It is absolutely necessary in the process of tapping the API, processing the data and passing it to View.

The process of inserting an if in a for statement can be rewritten as for + where.

Suppose you have these animals.

protocol Animal {
    var name: String { get }
}

protocol Runable: Animal {
    func run()
}
extension Runable {
    func run() {
        print("run!!!")
    }
}

class Cat: Animal, Runable {
    var name: String = "Cat"
}

class Dog: Animal, Runable {
    var name: String = "dog"
}

class Penguin: Animal {
    var name: String = "Penguins"
}

let animals: [Animal] = [Cat(), Dog(), Penguin()]

Before

for animal in animals {
    if animal is Runable {
        print("This animal can run!")
    }
    if let runableAnimal = animal as? Runable {
        runableAnimal.run()
    }
}

After

for animal in animals where animal is Runable {
    print("This animal can run!")
    
    if let runableAnimal = animal as? Runable {
        runableAnimal.run()
    }
}

If you master where, you will feel that you can do something.

for + where can be rewritten to filter + forEach.

From the previous example Before

for animal in animals where animal is Runable {
    print("\(animal.name)Can run!")
    
    if let runableAnimal = animal as? Runable {
        runableAnimal.run()
    }
}

After

animals.filter { $0 is Runable }.forEach {
    print("\($0.name)Can run!")
    
    if let runableAnimal = $0 as? Runable {
        runableAnimal.run()
    }
}

Higher-order function. In this example, it may be faster to cast and apply to compactMap.

animals.compactMap { $0 as? Runable }.forEach {
    print("\($0.name)Can run!")
    $0.run()
}

You can use inout when you want to reflect the change of the argument to the reference source.

This is so-called passing by reference.

Before

var fuga = "This is fuga."

print(fuga) //This is fuga.

func addHogeString() {
    fuga.append("\n")
    fuga.append("add hoge")
}

addHogeString() 

print(fuga) //This is fuga.\n Add hoge.

After

func addHogeString(strings: inout String) {
    strings.append("\n")
    strings.append("add hoge")
}

var fuga = "This is fuga."

print(fuga) //This is fuga.

addHogeString(strings: &fuga)

print(fuga) //This is fuga.\n Add hoge.

This expression of passing by reference using & is also used in Combine's assign.

filter + first or last can be written withfirst (where :) or last (where :).

Before

animals.filter { !($0 is Runable) }.first // Penguin

After

animals.first { !($0 is Runable) } // Punguin

Somehow, using first is better for performance.

StackView can be used to reduce constraints.

Suppose you want to prepare such a View. (The color is changed so that the frame is easy to see.)

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 a little. Please note that some people say that if you use it too much, the performance will drop.

With StackView + ScrollView, you can easily build a scrollable layout that is easy to expand.

UIScrollView is used to create a screen that you want to scroll like this, but ScrollView determines whether scrolling occurs depending on the size of the View inside, so you have to prepare a View inside. If you make that View a StackView, the size will change just by addingArreangedSubView to the stackView, so scrolling will occur easily without having to calculate the height of the View.

After that, just add View to stackView and scrolling will occur when the size of StackView becomes larger than the screen size.

No scrolling With scrolling

You can use hittest when you want to get the touch event of the View behind the visible View

Suppose you have a screen with these ScrollViews overlapping.

The image is the screen of the app that records the position of the rock currently under development.

https://github.com/kawano108/RockMap

Before If it is left as it is, the vertical ScrollView on the front absorbs the touch event, and the horizontal ScrollView on the back does not respond.

After So hittest. You can ignore the touch event by overriding it to return nil.


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

You can also touch the horizontal Scroll View on the back side.

I was impressed.

You can use PropertyWrapper to annotate the same process and omit it.

For example, set and get processing to Keychain. Before Suppose you get and set like this.

final class KeychainManager {
    
    ///Key
    struct Key {
        static let accessToken = "accessToken"
    }

    ///Keychain instance
    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("Failed to get the data from 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("Failed to save data to Keychain. ..")
        }
    }
}

//When using
KeychainManager.accessToken = accessToken // set
let accessToken = KeychainManager.accessToken

After If you use propertyWrapper, you don't have to write set or 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?

}

//When using
KeychainDataHolder.shared.accessToken = accessToken // set
let accessToken = KeychainDataHolder.shared.accessToken //get

If you increase the key and properties, you don't have to write set and get.

At the end

Thank you for reading! I think there are more, but when I try to put it out, it doesn't come out. Tomorrow is @takashico!

Recommended Posts

[For those who want to do their best in iOS from now on] Summary of the moment when I was surprised after developing an iOS application for two and a half years
For those who want to use MySQL for the database in the environment construction of Rails6 ~.
Create an Android app for those who don't want to play music on the speaker
I tried to develop a web application from a month and a half of programming learning history
I want to import the pull-down menu items when submitting a form in Rails into CSV and display them from the DB data.
Approximately three and a half months after joining the company, an output article to feed on the reviews
What I did when I was addicted to the error "Could not find XXX in any of the sources" when I added a Gem and built it