Category: ‘Swift’

RxSwiftのシンプルな説明

2018年8月9日 Posted by PURGE

RxSwiftのシンプルな説明

Swiftで簡単なアプリを写経するのは簡単なのであるが、やはり実用的なアプリを作成するには、レスポンシブでないと役な立たない(お客さん受けしない)よねということで、RxSwiftを学んでみた。

正直、3日間くらい、泣きながら、挫けそうになりながら、禿げそうになりながら食らいついていた。

はっきりいって理解するのがキツイ。

■ 開発環境
OS : MacOS High Sierra Version 10.13.6
Xcode : Version 9.4.1

■ サンプリアプリ
ボタンを押すとカウントアップするだけのアプリ。

スクリーンショット 2018-08-09 14.11.12.png

■ ソースコード解説
ViewController.swiftのコードを説明する。

まずは、必要なライブラリをimportする。ここでは、RxSwiftを利用する。

import UIKit
import RxSwift

先に、ラベルとボタンを配置する。
StreatBoardでも良いのであるが、敢えてコードで記述。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Label
        let countLbl = UILabel()
        countLbl.text = "0"
        countLbl.textAlignment = .center
        countLbl.textColor = UIColor.blue
        countLbl.frame = CGRect(x: (self.view.frame.width-110)/2, y: 150, width: 110, height: 21)

        // Button
        let countUpBtn = UIButton()
        countUpBtn.frame = CGRect(x: (self.view.frame.width-100)/2, y: 200, width: 100, height: 30)
        countUpBtn.layer.borderWidth = 1.0
        countUpBtn.layer.borderColor = UIColor.blue.cgColor
        countUpBtn.layer.cornerRadius = 5.0
        countUpBtn.setTitleColor(UIColor.blue, for: .normal)
        countUpBtn.setTitle("カウントUP", for: .normal)
        countUpBtn.addTarget(self, action: #selector(ViewController.countUp(_:)), for: UIControlEvents.touchUpInside)

        self.view.addSubview(countLbl)
        self.view.addSubview(countUpBtn)
}

次に、RxSwiftの肝であるクラスを作成する。

class RxSwiftSample {

}

RxSwiftSampleクラスには、private宣言されたsubjectPublishSubject<Int>()でインスタンス化して保持しておく。それと、そのsubjectをObservableとして返すeventを定義する。
そして、実際の処理を外部から呼べる myFunc()メソッドを定義する。その中で、onNextとかいうイベントを発行する。

これが基本的な雛形という感じかな?

class RxSwiftSample {
    private let subject = PublishSubject<Int>()

    var event : Observable<Int>{
        return subject
    }
    
    func myFunc(){
        // 処理
        print("myFuncが呼ばれました。")
        // イベント発行
        subject.onNext(self.data)
    }
}

次に、このRxSwiftSampleクラスの使い方なのであるが、ViewControllerから利用できるように、RxSwiftSampleをモデルとしてmodelで宣言しておく。また、後で利用するdisposeBagも宣言しておく。

private let model = RxSwiftSample()
private let disposeBag = DisposeBag()

上記を、ViewControllerから利用する。
先ずは、順を追ってわかりやすく、ボタンに対して、countUp(_ sender: UIButton)が呼ばれるように記述する。これは特に問題はないはず。
重要なのは、ここからmodelmyFunc()を呼び出すことである。

    override func viewDidLoad() {
     //ボタンへイベント登録    
        countUpBtn.addTarget(self, action: #selector(ViewController.countUp(_:)), for: UIControlEvents.touchUpInside)
    }

    @objc func countUp(_ sender: UIButton){
        print("カウントアップ")    
        //Modelの関数呼び出し
        model.myFunc()
    }

ここから、理解が進むと思うが、model.myFunc()で処理を行うと、事前に登録されているsubscribeで通知される。そして、 subject.onNext(self.data)で渡されたパラメータも、value で受け取れる。

        //Reactive処理
        model.event.subscribe(
                onNext: {value in
                    print("ここで通知")                
                    countLbl.text = String(value)
                })
                .disposed(by: disposeBag)

これで、ようやく頭の中で処理シーケンスがつながった。
それでも理解できない場合は、私と同じように1日悩んで、print()仕掛けて、処理を追ってみると良いと思う。

やはり、このようなフレームワークやデザインパターンの理解は正直キツイ。それでも、時間を掛けて手を動かすのが、ベストプラクティスだと思う。

その助けになればと祈る。情報がちょっと時代遅れ感はあるのだが・・・。
始めたのが遅いので仕方ない。w

■ 最後に

ここに記したコードは、自分がRxSwiftの構造を理解するために作成したコードです。間違い等がございましたらご指摘願います。

下記、全ソースコードです。

import UIKit
import RxSwift


private let model = RxSwiftSample()
private let disposeBag = DisposeBag()

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Label
        let countLbl = UILabel()
        countLbl.text = "0"
        countLbl.textAlignment = .center
        countLbl.textColor = UIColor.blue
        countLbl.frame = CGRect(x: (self.view.frame.width-110)/2, y: 150, width: 110, height: 21)

        // Button
        let countUpBtn = UIButton()
        countUpBtn.frame = CGRect(x: (self.view.frame.width-100)/2, y: 200, width: 100, height: 30)
        countUpBtn.layer.borderWidth = 1.0
        countUpBtn.layer.borderColor = UIColor.blue.cgColor
        countUpBtn.layer.cornerRadius = 5.0
        countUpBtn.setTitleColor(UIColor.blue, for: .normal)
        countUpBtn.setTitle("カウントUP", for: .normal)
        countUpBtn.addTarget(self, action: #selector(ViewController.countUp(_:)), for: UIControlEvents.touchUpInside)

        self.view.addSubview(countLbl)
        self.view.addSubview(countUpBtn)

        //Reactive処理
        model.event.subscribe(
                onNext: {value in
                    countLbl.text = String(value)
                })
                .disposed(by: disposeBag)
    }

    @objc func countUp(_ sender: UIButton){
        //Modelの関数呼び出し
        model.myFunc()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

}

class RxSwiftSample {
    private let subject = PublishSubject<Int>()
    private var data = 0

    var event : Observable<Int>{
        return subject
    }

    func myFunc(){
        // 処理
        data = data + 1

        // イベント発行
        subject.onNext(self.data)
    }
}

Lovly Swift!!!

Alamofireで取得したデータをTableViewで表示する

2018年8月9日 Posted by PURGE

3年以上ぶりに、iOSの開発を行い始めた。今の時代はswift4 なのね。

■ 開発環境
OS : MacOS High Sierra Version 10.13.6
Xcode : Version 9.4.1

■ Server

サーバサイドは、JSONデータをRuby on Railsのように、DBスキーマから簡単に返すことができるので、Elixir言語で書かれた Phoexnix framework を利用しています。
その辺りは、後日に。

■ 取得データ
リクエスト送信先から返ってくるJSONデータは下記の感じ。

{
  "users" : [
    {
      "option" : "システムエンジニア",
      "age" : 46,
      "name" : "田島 啓之",
      "id" : 1
    },
    {
      "option" : "経理",
      "age" : 49,
      "name" : "都築 奏子",
      "id" : 2
    },
    {
      "option" : "ネットワークエンジニア",
      "age" : 45,
      "name" : "中村 栄人",
      "id" : 3
    },
    {
      "option" : "総務",
      "age" : 38,
      "name" : "千葉 博子",
      "id" : 4
    }
  ]
}

■ ソースコード解説

ListViewController.swiftのコードを説明する。

まずは、必要なライブラリをimportする。ここでは、AlamofireSwiftyJSONを利用する。

import UIKit
import Alamofire
import SwiftyJSON

次に、Swift4から、Codableというもので、構造体とJSONデータを簡単にマッピングできるようになったらしいので、Codableを利用して下記のような構造体を定義する。
これは、JSONデータのネスト構造を見てわかるように、Users構造体の中に、User構造体のリストが定義されている構造となる。

struct User : Codable{
    let id : Int
    let name: String
    let age : Int
    let option : String
}
struct Users : Codable{
    let users : [User]
}

ListViewControllerクラスは、UITableViewDelegateUITableViewDataSourceを採用する。特に問題はない。

class ListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

各メソッドから参照可能なように、Users構造体を定義する。初期化時には値が決まらないため、nilを許すため、オプショナル型としている。

    var users : Users?

viewDidLoad()においてUIの初期化を行う。

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tableView : UITableView!
        tableView = UITableView(frame: view.frame, style: .grouped)
        tableView.delegate = self
        tableView.dataSource = self
        
        view.addSubview(tableView)

Alamofireの主要処理部分。まずは、対象サーバへリクエストを送信して、JSONで受け取る。レスポンスがnilの場合は、そのままreturn

レスポンスがnilでない場合は、JSONDecoderで、Users構造体へマッピングする。

tableView.reloadData()は、Alamofireで取得するデータは非同期のため、初回は、tableViewは空のため、読み込みが完了した時点で、tableViewをリロードする必要がある。

        //Alamofire
        Alamofire.request("http://localhost:4000/user")
            .responseJSON{res in
                guard let json = res.data else{
                    return
                }
                self.users = try! JSONDecoder().decode(Users.self, from: json)
                
            tableView.reloadData()
        }

ここでは、リストの数を返す必要がある。通信が非同期のため、この時点では構造体はnilなので、初回は0を返す必要がある。リロードされた時に、Users構造体にデータはセットされているので、users.count でデータ数が取得できる。

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let cnt = self.users?.users.count{
            return cnt
        }
        return 0
    }

ここでは、セルのデータを設定している。やはりここも、初回はnilのため、let if で、nilチェックを行っている。
データがあれば、cell.textLabel.text を名前を設定する。

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CELL")
        if let user = self.users?.users[indexPath.row]{
            cell.textLabel?.text = user.name
        }
        return cell
    }

■ 最後に

ここに記したコードは、初回時のデータ取得から表示までのサンプル的な最低限のコードです。もう少しリファクタリングして、構造を設計する必要がありますが、とりあえず動作原理をシンプルに理解するために記載しました。

間違い等がございましたら、ご指摘願います。

下記、全ソースコードです。

ListViewController.swift

import UIKit
import Alamofire
import SwiftyJSON

struct User : Codable{
    let id : Int
    let name: String
    let age : Int
    let option : String
}
struct Users : Codable{
    let users : [User]
}

class ListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    var users : Users?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tableView : UITableView!
        tableView = UITableView(frame: view.frame, style: .grouped)
        tableView.delegate = self
        tableView.dataSource = self
        
        view.addSubview(tableView)
        
        //Alamofire
        Alamofire.request("http://localhost:4000/user")
            .responseJSON{res in
                guard let json = res.data else{
                    return
                }
                self.users = try! JSONDecoder().decode(Users.self, from: json)
                
                //Debug
                print("--- User JSON ---")
                print(JSON(json))
                print("--- User Info ---")
                for user in self.users!.users{
                    print("NAME:" + user.name)
                    print("AGE:" + String(user.age))
                }
                
            tableView.reloadData()
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let cnt = self.users?.users.count{
            return cnt
        }
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CELL")
        if let user = self.users?.users[indexPath.row]{
            cell.textLabel?.text = user.name
        }
        return cell
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
}

Lovly Swift!!!

Swift4 ボタンとラベルのコード

2018年8月9日 Posted by PURGE

覚え書き。

    override func viewDidLoad() {
        super.viewDidLoad()
        // Label
        let countLbl = UILabel()
        countLbl.text = "ラベル"
        countLbl.textAlignment = .center
        countLbl.textColor = UIColor.blue
        countLbl.frame = CGRect(x: (self.view.frame.width-110)/2, y: 150, width: 110, height: 21)

        // Button
        let countUpBtn = UIButton()
        countUpBtn.frame = CGRect(x: (self.view.frame.width-100)/2, y: 200, width: 100, height: 30)
        countUpBtn.layer.borderWidth = 1.0
        countUpBtn.layer.borderColor = UIColor.blue.cgColor
        countUpBtn.layer.cornerRadius = 5.0
        countUpBtn.setTitleColor(UIColor.blue, for: .normal)
        countUpBtn.setTitle("ボタン", for: .normal)
        countUpBtn.addTarget(self, action: #selector(ViewController.countUp(_:)), for: UIControlEvents.touchUpInside)

        self.view.addSubview(countLbl)
        self.view.addSubview(countUpBtn)

    }

Xcode Alamofire で通信エラー

2018年8月4日 Posted by PURGE

下記のエラーが出た。

The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

まあこれは、有名なお話らしく、iOS9 以降のセキュリティポリシーで、http通信が原則禁じられているらしい。なので、Webviewのアプリでも生じるらしい。

対応方法としては、Info.plist ファイルに「App Transport Securith Settings」を追加して、http通信を可能に設定するということ。

基本的には、開発時のみの設定としておくのが望ましい。

SwiftでAFNetworkingを利用してJSONデータ取得の覚え書き。

2015年1月24日 Posted by PURGE

結構理論的にはわかっているが、いざソースを書いてみるとかなりハマる。
オプショナル型の制約を熟知していないと、むしろ面倒でバグを誘発するのではないか?

とりあえず、Qiitaに記載する前のドラフト版のソースコード。
エラーが発生しないように、エラーを避けるようなコードを記述しているので結構不安。

MyTableViewController.swift

import UIKit

struct Game {
    init(){}
    var seasonId:String?
    var gameName:String?
    var gameType:String?
    var homeTeamName:String?
    var awayTeamName:String?
    var homeTeamScore: String?
    var awayTeamScore: String?
}

class MyTableViewController: UITableViewController {

    required init(coder aDecoder: NSCoder) {
        self.gameData = Game()
        super.init(coder: aDecoder)
    }

    var gameData:Game
        
    override func viewDidLoad() {
        super.viewDidLoad()

        //リクエストデータ取得
        let manager:AFHTTPRequestOperationManager = AFHTTPRequestOperationManager()
        let serializer:AFJSONRequestSerializer = AFJSONRequestSerializer()
        manager.requestSerializer = serializer
        manager.GET("http://score-sheet.herokuapp.com/api/games/latest.json", parameters: nil,
            success: {(operation: AFHTTPRequestOperation!, responsObject: AnyObject!) in
                let responsDict = responsObject as Dictionary<String, AnyObject>
                
                //値取得
                var seasonId:Int = (responsDict["season_id"] as AnyObject?) as Int
                var gameName:String = (responsDict["game_name"] as AnyObject?) as String
                var gameType:Dictionary = (responsDict["game_type"] as AnyObject?) as Dictionary<String, AnyObject>
                var gameTypeName:String = (gameType["game_type"] as AnyObject?) as String
                var homeTeam:Dictionary = (responsDict["home_team"] as AnyObject?) as Dictionary<String, AnyObject>
                var awayTeam:Dictionary = (responsDict["away_team"] as AnyObject?) as Dictionary<String, AnyObject>
                var homeTeamName:String = (homeTeam["team_name"] as AnyObject?) as String
                var awayTeamName:String = (awayTeam["team_name"] as AnyObject?) as String
                var homeTeamScore = (responsDict["home_team_score"] as AnyObject?) as Int
                var awayTeamScore = (responsDict["away_team_score"] as AnyObject?) as Int
                
                //取得データ設定
                self.gameData.seasonId = String(seasonId)
                self.gameData.gameName = gameName
                self.gameData.gameType = gameTypeName
                self.gameData.homeTeamName = homeTeamName
                self.gameData.awayTeamName = awayTeamName
                self.gameData.homeTeamScore = String(homeTeamScore)
                self.gameData.awayTeamScore = String(awayTeamScore)
                
                //データリロード
                self.tableView.reloadData()

                //テーブルビュー作成
                let tableView = MyTableView(frame: self.view.frame, style: UITableViewStyle.Plain)
                tableView.delegate = self
                tableView.dataSource = self
                self.view.addSubview(tableView)
                
                //セル設定
                let xib = UINib(nibName: "MyTableViewCell", bundle: nil)
                tableView.registerNib(xib, forCellReuseIdentifier: "BfCell")
            },
            failure: {(operation: AFHTTPRequestOperation!, error: NSError!) in
                println("Error!!")
            }
        )
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // #warning Potentially incomplete method implementation.
        // Return the number of sections.
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete method implementation.
        // Return the number of rows in the section.
        return 5
    }
    
    //セルデータ設定
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("BfCell", forIndexPath: indexPath) as MyTableViewCell
        
        cell.seasonId?.text = self.gameData.seasonId
        cell.gameName?.text = self.gameData.gameName
        cell.gameType?.text = self.gameData.gameType
        cell.homeTeamName?.text = self.gameData.homeTeamName
        cell.awayTeamName?.text = self.gameData.awayTeamName
        cell.homeTeamScore?.text = self.gameData.homeTeamScore
        cell.awayTeamScore?.text = self.gameData.awayTeamScore
        
        return cell
    }

    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        //let cell = tableView.dequeueReusableCellWithIdentifier("BfCell") as UITableViewCell
        //return cell.bounds.height
        return 100
    }
    /*
    // Override to support conditional editing of the table view.
    override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return NO if you do not want the specified item to be editable.
        return true
    }
    */

    /*
    // Override to support editing the table view.
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            // Delete the row from the data source
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
        }    
    }
    */

    /*
    // Override to support rearranging the table view.
    override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {

    }
    */

    /*
    // Override to support conditional rearranging of the table view.
    override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return NO if you do not want the item to be re-orderable.
        return true
    }
    */

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        // Get the new view controller using [segue destinationViewController].
        // Pass the selected object to the new view controller.
    }
    */
    
}

Unable to run app in Simulator

2014年9月7日 Posted by PURGE

ビルドは上手くいくが、下記のエラーが出力されるときの対応。

Unable to run app in Simulator

An error was encountered while running (Domain = FBSOpenApplicationErrorDomain, Code = 4)

スクリーンショット 2014-09-07 21.15.03

iOSシミュレータの下記メニューからコンテンツをリセットする。

 [iOS Simulator]-[Rest Contents and Settings…]-[Reset]

スクリーンショット 2014-09-07 21.20.56

Swift 事始め その2

2014年6月7日 Posted by PURGE

次は、クラス継承。

class Animal{
    var name:String
    var age:Int = 0
    //イニシャライザ(コンストラクタ)
    init(name:String){
        self.name = name
    }
    //メソッド    
    func call()->String{
        return "Hello " + name + "!!"
    }
}

//クラス継承
class Dog : Animal{
    
}

var dog = Dog(name: "ぽち")
println(dog.call())

Swift 事始め

2014年6月7日 Posted by PURGE

2014年6月3日に、Apple社から新言語 Swift が発表された。
そこで、Swift が現在の言語の素敵な部分を多く取り込んでおり、しかも完成度も高いので、今後に期待する意味で勉強を始めた。

前回までの自分の興味は、Python だったのに…。

ちなみに、1914年6月7日時点では、XCode6-Beta版のみでサポートされているようです。
しかも、このXCode6-Beta版は、Apple Developer Program (有償)に参加していないとダウンロードできないようです。

しばらくiOS開発に後ろ向きだったが、これを機会に加入してしまいました。それだけ熱い。

まずは、定番の Hello world!! から。

var str = "Hello "
var str2 = "whoocus !!"
println(str + str2)

そして、同じく定番であるクラスの使用

class Animal{
    var name:String
    var age:Int = 0
    //イニシャライザ(コンストラクタ)
    init(name:String){
        self.name = name
    }
    //メソッド    
    func call()->String{
        return "Hello " + name + "!!"
    }
}
//Animalインスタンス化
var dog = Animal(name: "犬")
println(dog.name)

Javaユーザにも優しい言語ですね。