[SWIFT] Three problems that beginners are likely to fall into when developing iOS apps and their analysis

Introduction

Hello, this is the iOS engineer dayossi.

I want to provide a service that makes my family happy We have released a family diary app called HaloHalo.

This time, I felt that I was developing an application through unit tests. I analyzed three problems.

The deliverable this time is here. https://github.com/Kohei312/TDD_Practice_Poker

Trouble that I often fell into

When developing without writing tests, I couldn't separate the following three problems, and I was often troubled.

・ Is the logic of View construction centered on UIKit strange ...? -Is the data for displaying the UI strange ...? ・ Is the data transfer strange ...?

By writing unit tests Separation around here can be done smoothly, It's easier to analyze the problem.

When analyzing the problem, the following three perspectives I felt that it was effective in common.

1. Are you able to separate responsibilities properly?

One of the SOLID principles, which is an object-oriented principle, `` `single responsibility principle ```, This is a question to confirm whether you are aware of the design policy and intention.

This time, I used it with the nuance of "clarify the purpose for each layer and process".

2. Is it possible to visualize the change in state?

There is a part that overlaps with the first point, It is to clarify which process is called at what timing and what kind of change is occurring.

Especially when there are many arguments and you try to perform multiple processes at the same time, it is easy to get confused. Therefore, I felt that code mistakes were likely to occur.

If you enumerate the states with enum or define the process to change the property only in a specific layer, It was easy to grasp the state change.

3. Are the layers divided appropriately?

This also overlaps with the first point, I checked sequentially whether the layers were working according to the overall design.

While checking the purpose of what state each layer manages Gradually separating the responsibilities made it easier to review.

Below, we will take up the cases that we actually worked on in this application development.

Case: State is not shared

Among the poker games I created this time We have set a rule that players can exchange cards up to 3 times. (There are two players, a user and a CPU)

Among them, according to the number of times each player exchanged cards A stumbling block occurred in that the state of the game changed.

The number of exchanges is a variable called changeCount Hold it as a property of a structure that has a player state called Player, I was trying to control according to the rules.

Whether the player is a user or a computer in an enum called PlayerType I try to distinguish.

public enum PlayerType{
    case me
    case other
}
struct Player{
    
    var playerType:PlayerType
    
    init(playerType:PlayerType){
        self.playerType = playerType
    }
    
    var playerStatement:PlayerStatement = .thinking
    var changeCount = 3
}

Initially in a higher layer called PokerInteractor that manages the logic of the entire game I placed and managed a Player type instance directly.


 // MARK:-Manage the number of card exchanges and status of each player
public struct PokerInteractor{

    var player_me:Player
    var player_other:Player
    
    mutating func changePlayerCount(_ playerType:PlayerType){ 
       switch playerType{
         case .me:
              player_me.changeCount -= 1
         case .other:
              player_other.changeCount -= 1
       }
    }

Positioned as a layer that organizes business logic, Here I was controlling the state of the player and the progress of the game.

However, there was a pitfall here.

On one player's turn, that player's card exchange count was properly counted. The other player's card exchange count is not shared.

At player_me's turn, player_me's changeCount is certainly decreasing At the turn of player_other, the changeCount of player_me has returned to the initial value.

public struct PokerInteractor{

# WARNING ("State is never shared ...")
    var player_me:Player
    var player_other:Player
    
    mutating func changePlayerCount(_ playerType:PlayerType){ 
       switch playerType{
         case .me:
              player_me.changeCount -= 1
         case .other:
              player_other.changeCount -= 1
       }
    }

Problem: There is a problem in managing logic data

In the test, it was confirmed that the calculation was actually possible. I didn't see any build errors on the View side, so I thought there was a problem managing the logic data.

Analysis: Could not consider changing memory pointers

Because changeCount is an immutable value When changing the value, it is necessary to instruct the change from the generated instance of Player type.

However, if you update the property of Player which is a value type, the value of the entire Player will be updated. The upper layer PokerInteractor with Player as a property is also updated.

Therefore, the two players managed by PokerInteractor As a result, a new instance will be regenerated,

The card exchange count has been reset to the initial value every time It was no longer possible to share the status of all players.

Therefore, add one reference type PlayerStatus to grasp the status of all Players. It has been changed so that the player status can be shared.

Workaround: Added a reference layer between Player and PokerInteractor

Because the memory area that refers to PlayerStatus is always the same Even if the value of each player is updated and the memory pointer is changed The aim was to always be able to scope the changed value.


final class PlayerStatus{    

    var players:[Player] = []
    var interactorInputProtocol:InteractorInputProtocol?
    
    subscript(playerType:PlayerType)->Player{
        get{
            return players.filter({$0.playerType == playerType}).last!
        }
        set(newValue){
            if let player = players.filter({$0.playerType == playerType}).last{
                for (index,p) in players.enumerated() {
                    if p.playerType == player.playerType{
                        players.remove(at: index)
                        players.insert(newValue, at: index)
                    }
                }
            }
        }
    }

    func decrementChangeCount(_ playerType:PlayerType){
        
        self[playerType].changeCount -= 1
        interactorInputProtocol?.checkGameStatement(playerType)
        
    }
}

I stored the Player class in an array so that I can extract the required properties with subscript. Since the calculation cost is high and the nesting is difficult to read deeply,

In the case where the characters are limited like this app, It is better to keep each instance separately I'm glad it was easy to understand.

As a caveat, change the PlayerStatus property from anywhere Because you can share that state The calculation process is also unified so that it is performed from Player Status.

Discussion: The separation of responsibilities of each module was ambiguous

With the above, it is also clarified that Player Status is responsible for managing the status of each player. From PokerInteractor, I just instructed to change the state.

In other words, in PokerInteractor, which manages business logic It can be said that he overlooked that the responsibilities could be dispersed.

Through the test, each process of business logic Because I was able to confirm that it was working properly

The responsibilities of the PokerInteractor layer are becoming more complex I think I was able to notice it.

Summary

In my experience, I extracted 3 patterns that are easy to fall into. It's really embarrassing because it's just the basics.

I was able to realize once again that the majority of the parts were out of principle and difficult. We will do our best to make better use of the design principles.

We look forward to your warm tsukkomi.

Reference books / articles

[TDD Boot Camp.TDDBC Sendai 07 Challenge: Poker] (http://devtesting.jp/tddbc/?TDDBC%E4%BB%99%E5%8F%B007%2F%E8%AA%B2%E9%A1%8C)

Kazuaki Matsuo, Yusuke Hosonuma, Kenji Tanaka et al. IOS Test Complete Book (2019). PEAKS Publishing.

Yoshitaka Seki, Shoshin Fumi, Kenji Tanaka et al. Introduction to iOS App Design Patterns (2019). PEAKS Publishing.

[Kenji Tanaka. TDD (2018). Impress R & D.] (https://nextpublishing.jp/book/10137.html)

[keyword] TDD Driven Design: [Dan Chaput, Lee Lambert, Rich Southwell. What is an Enterprise Business Rule Repository?. MODERA analyst.com.] (http://media.modernanalyst.com/New_Wisdom_Software_Webinar_-_PRINT.pdf) Value Semantics: What is Yuta Koshizawa. Value Semantics. Heart of Swift Yuta Koshizawa. Types without Value Semantics and Workarounds. Heart of Swift [Yuta Koshizawa. Why Swift has become a value-centric language and how to use it. Heart of Swift](https://heart-of-swift.github.io/value-semantics/how-to-use-value- types) Copy-on-Write: (I don't think Copy-on-Write is a problem in Swift) [https://qiita.com/koher/items/8c22e010ad484d2cd321] (Explanation of how to implement Copy on Write in Swift) [https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd] (Design that is easy to test using the principle of dependency reversal in swift) [https://qiita.com/peka2/items/4562456b11163b82feee]

VIPER: (Summary of making iOS app of product from 1 with VIPER architecture) [https://qiita.com/hirothings/items/8ce3ca69efca03bbef88]

SOLID analysis: (SOLID principle understood by Swift iOSDC 2020) [https://speakerdeck.com/k_koheyi/swifttewakarusolidyuan-ze-iosdc-2020] (Explanation of the SOLID principle in the case of iOS development) [https://zenn.dev/k_koheyi/articles/019b6a87bc3ad15895fb]

memory: (Examine Swift's memory layout) [https://qiita.com/omochimetaru/items/64b073c5d6bcf1bbbf99] (Great Swift pointer type commentary) [https://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052] (Memory Safety) [https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html#//apple_ref/doc/uid/TP40014097-CH46-ID571]

Value type: Let's know the difference between mutable type and immutable type [Pure value type Swift] (https://qiita.com/koher/items/0745415a8b9842563ea7)

subscript: About Swift Subscript

protocol oriented: Making better apps with value types at WWDC 2015 Swift

enum: Swift enum (enum) review [Swift] enums are protocol compliant, so you can simply compare them, for example with Comparable

Recommended Posts

Three problems that beginners are likely to fall into when developing iOS apps and their analysis
About "Dependency Injection" and "Inheritance" that are easy to understand when remembered together