なんとかするから、なんとかなる

エンジニア関係のことを書きます

iOS UIPageViewController How to soleve the error message "Unbalanced calls to begin/end appearance transitions for" .

Introduction

When I tried to transition some pages so rapidly.

The Xcode shows an error message as below.

Unbalanced calls to begin/end appearance transitions for 

The code is below.

class MyPageViewController: UIPageViewController {
    var maxPage = 3
    var currentPage = 0

    ~~~~
    // SomeCode....
    ~~~~
}

extension MyPageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard self.currentPage > self.maxPage else { return nil }
        self.currentPage += 1
        
        let myAfterVC = MyAfterViewController()
        
        return myAfterVC
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard self.currentPage >=  0 else { return nil }
        self.currentPage -= 1
        
        let myBeforeVC = MyBeforeViewController()
        
        return myBeforeVC
    }
}

Why

This error message is showed up when a new transition begin before an old old is not completed.

Solution

To prepend a new transition before an old one is not completed, I added a transitioning flag in MyPageViewController and change the value in UIPageVieController Delegate.

The true code is below.

class MyPageViewController: UIPageViewController {
    var isTransitioning = false
    var maxPage = 3
    var currentPage = 0

    ~~~~
    // SomeCode....
    ~~~~
}

extension MyPageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard self.currentPage < self.maxPage, self.isTransitioning == false else { return nil }
        self.currentPage += 1
        
        let myAfterVC = MyAfterViewController()
        
        return myAfterVC
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard self.currentPage >=  0, self.isTransitioning == false else { return nil }
        self.currentPage -= 1
        
        let myBeforeVC = MyBeforeViewController()
        
        return myBeforeVC
    }
}


extension MyPageViewController: UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
        self.isTransitioning = true
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        self.isTransitioning = false
    }
}

iOS How to set the Autolayout programatically

Introduction

It was difficult and complexity to set the autolayout programatically when the AutoLyaout announced in iOS

NSLayoutConstraint(item: button, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true

That was because I used to use Snapkit(OSS).

github.com

But, these days, it will easy to set the autolayout programatically.

I'll introduce the method of setting autolayout programatically.

How to set the autolayout programatically

For example, if you want to the view at the bottom of the self.view, you can easy to set it below.

Code

let button = UIButton()
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
button.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.06).isActive = true
button.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20).isActive = true

Attention

order to calling the addSubview()

If you register the constraint to self.view before calling addSubview method, the app will crash.

Don't forget setting the translatesAutoresizingMaskIntoConstraints

If you forget to set the translatesAutoresizingMaskIntoConstraints to false, the constraint will not working.

Don't forget setting "isActive = true"

If you forget to set the isActive propery, the constraint will not working.

How to set the autolayout to SafeArea programatically

To set the autolayout to Safeare, you can use safeAreaLayoutGuide.

if #available(iOS 11, *) {
    button.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
} else {
    button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20).isActive = true
}

How to make a margin related with other view's height by ration.

If you want to set the UIButton at the top of self.view with margin "self.view.frame.size.height * 0.3", you can use the UILayoutGuide.

In programatically autolayout setting, it not need to use UIView for spacer anymore.

let spacer = UILayoutGuide()
self.view.addLayoutGuide(spacer)
spacer.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
spacer.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
spacer.topAnchor.constraint(equalTo: button.bottomAnchor).isActive = true

Finally

I'll show the whole code to initialize the UIButton instance and set the autolayout programatically.

lazy var mybutton: UIButton = {
    let button = UIButton()
    self.view.addSubview(button)
    button.translatesAutoresizingMaskIntoConstraints = false
    button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    button.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.06).isActive = true
    button.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
        
    let spacer = UILayoutGuide()
    self.view.addLayoutGuide(spacer)
    spacer.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
    spacer.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
    spacer.topAnchor.constraint(equalTo: button.bottomAnchor).isActive = true
     
    return button
} ()

iOS AutoLayoutをコードから設定する方法

English version below

はじめに

AutoLayoutが発表された当初、AutoLayoutをコードから設定するのは面倒でした。

NSLayoutConstraint(item: button, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true

つらい。

そこでよくSnapKitというOSSを使っていました。

github.com

しかし、最近はOSSを使わなくとも簡単に設定することができるため、その方法を備忘録として

やり方

viewの下端にUIButtonを設置したい場合は、次のように設定します。

コード

let button = UIButton()
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
button.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.06).isActive = true
button.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20).isActive = true

注意点

addSubview()の順番

addSubviewでSubviewに登録する場合にconstraitを設定すると落ちます。

translatesAutoresizingMaskIntoConstraintsの設定忘れ

translatesAutoresizingMaskIntoConstraintsをfalseに設定しないと制約が反映されません。

isActive = trueの設定忘れ

isActiveを設定しないと制約は有効になりません。

SafeAreaの設定方法

セーフエリアの設定には、safeAreaLayoutGuideを使用します。

if #available(iOS 11, *) {
    button.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
} else {
    button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20).isActive = true
}

viewに対する比率を指定して空き(マージン)を作る

AutoLayoutを使用していると、例えばUIButtonをviewの高さ1/3のところに設定したい。

など、あるViewに対する比率分空けたいなど比率を指定したいこともあります。

その場合はUILayoutGuideを使います。

空欄のUIViewを置くという方法もありますが、私はあまり好きではないです。

次のコードはUIButton(self.button)をViewの下部から高さ*0.2空けて表示するときのUILayoutGuideです。

let spacer = UILayoutGuide()
self.view.addLayoutGuide(spacer)
spacer.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
spacer.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
spacer.topAnchor.constraint(equalTo: button.bottomAnchor).isActive = true

最後に

最後にコード全体と示します。 UIButtonを宣言して、Autolayoutを設定するまでのコードです。

lazy var mybutton: UIButton = {
    let button = UIButton()
    self.view.addSubview(button)
    button.translatesAutoresizingMaskIntoConstraints = false
    button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    button.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.06).isActive = true
    button.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
        
    let spacer = UILayoutGuide()
    self.view.addLayoutGuide(spacer)
    spacer.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
    spacer.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
    spacer.topAnchor.constraint(equalTo: button.bottomAnchor).isActive = true
     
    return button
} ()

iOS How to use the Decodable

Introdution

This article show the basically usage of Decodable Protocol.  

What's the Decodable

The Decodable give us an easy way to convert the JSON Data type to Class or Struct.   Thanks for the Decoable protocol, developer could access each member value easily without any string key.   String Key oftem make us a typo.  It sometimes annoyed me ; )  

Let's try it!

Let's try to define the Docodable Struct

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
    }
}

 

Let's try to Decode the JSON data type

let json: Data = """
{
    "value": "GoodString",
    "app_version": 5
}
""".data(using: .utf8)!
let someStruct = try? JSONDecoder().decode(SomeStruct.self, from: json)

 

Details

At firts, apply the Decodable Protocol to Struct type.   Implement the enum of CodingKeys.   That's All!   CodingKeys need to include each values's name.   It the values name is different from the JSON Key value, you can declear the raw value in declaring of CodingKeys enum.   Important thing is that each values need declear in var instead of let.   If you declear the value in let, the decoding process allways return nil. ;)   If the some JSON key has optional key, it's ok to clear the member value with optional "?" .  

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    var exception: Int?
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
    }
}

 

Decode the JSON which has another struct.

Let's try to decode the JSON Data type which has another struct.   In this case, SomeStruct has InnerStruct Array.  

Declear the Decodable

 

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    var inners: [InnerStruct]
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
        case inners = "inner_structs"
    }
}
 
struct InnerStruct: Decodable {
    var index: Int
    var name: String
    
    enum CodingKeys: String, CodingKey {
        case index = "inner_index"
        case name = "inner_name"
    }
}

  If you would like to clear the InnerStruct as is, it's OK to clear it directly.

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    var inner: InnerStruct
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
        case inners= "inner_struct"
    }
}

 

Let's try to Decode the JSON data type

 

let json: Data = """
{
    "value": "GoodString",
    "app_version": 5,
    "inner_structs": [{
        "inner_index": 1,
        "inner_name": "myName"
    }]
}
""".data(using: .utf8)!
let someStruct = try? JSONDecoder().decode(SomeStruct.self, from: json)

 

Wrap up!

The decodable is easy and useful !!

iOS Decodableの基本的な使い方

English version below

はじめに

今回はSwift4 で追加されたDecodableの使い方について紹介します。  

Decodableとは

API Requestなどで返ってくるJSONのData型を任意のクラスや構造体に変換する際に便利です。

これまではSwiftyJSONなどライブラリーを使ってJSONの構造体にアクセスすることが多かったと思います。

しかし、そのままSwiftyJSONを使う場合、

  • 連想配列Dictionary に対して文字列を渡すことでタイポの危険性を伴う
  • 意図しないJSONでもJSONであれば問題無い

など、もやもやが僕の中にはありました。

Decodableを使うことで決まったJSONを決まったクラス/構造体にしか変換しないので、とてもわかりやすくて好きです。

Decodableを使って見る

構造体に対してDecodableのProtocolを適用する。

 

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
    }
}

 

試しにデコードして見る

 

let json: Data = """
{
    "value": "GoodString",
    "app_version": 5
}
""".data(using: .utf8)!
let someStruct = try? JSONDecoder().decode(SomeStruct.self, from: json)

 

詳細

構造体に対して、Decodableの適用を宣言。

CodingKeysという列挙体を宣言すると完了です。

CodingKeysはrawValueがStringの列挙体で、JSONの各Keyがどの変数に対応しているのかを宣言します。

CodingKeysのrawValueが変数名と等しい場合は、変数名をそのまま列挙体でも使用、異なる場合はrawValueに対してKey名を対応させます。

構造体の各変数はvarで宣言しないと、Decodeに失敗しnilが返ってきます。

hopita.hatenablog.com

JSONのKeyで存在の有無がOptionalの場合は構造体の変数宣言をOptionalにすればOKです。

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    var exception: Int?
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
    }
}

 

階層のあるJSONをデコードする

次に、階層構造のあるJSONについてです。 上記のSomeStruct の中にInnerStructが配列で存在すること場合、次のように宣言します。  

Decodableの宣言

 

struct SomeStruct: Decodable {
    var value: String
    var version: Int
    var inners: [InnerStruct]
    
    enum CodingKeys: String, CodingKey {
        case value
        case version = "app_version"
        case inners = "inner_structs"
    }
}
 
struct InnerStruct: Decodable {
    var index: Int
    var name: String
    
    enum CodingKeys: String, CodingKey {
        case index = "inner_index"
        case name = "inner_name"
    }
}

  今回はInnerStructは配列ですが、InnerStruct単体であれば[]を外せばOKです。  

デコードする

 

let json: Data = """
{
    "value": "GoodString",
    "app_version": 5,
    "inner_structs": [{
        "inner_index": 1,
        "inner_name": "myName"
    }]
}
""".data(using: .utf8)!
let someStruct = try? JSONDecoder().decode(SomeStruct.self, from: json)

 

おわりに

とても便利です。簡単です。使っていきましょう。