Jenkins更新検証環境をDockerで作る

Jenkins・pluginの更新をする際に検証できる環境が欲しい!となったので、Docker上に構築したメモ

成果物

github.com

やりたかったこと

  • できるだけ運用環境と近い状態から以下の検証ができるようにする
    • Jenkins自体の更新確認
    • pluginの追加・更新確認

運用環境

Docker Hubからjenkins imageを探す

  ❯ docker pull jenkins/jenkins:2.138.3
  2.138.3: Pulling from jenkins/jenkins
  Status: Downloaded newer image for jenkins/jenkins:2.138.3
  docker.io/jenkins/jenkins:2.138.3

  # コンテナの中に入る
  ❯ docker run -it --name jenkinstest jenkins/jenkins:2.138.3 /bin/bash
  jenkins@88681d8f77c5:/$

  jenkins@88681d8f77c5:~$ cat /etc/issue
  Debian GNU/Linux 9 \n \l

Debian だったのでこれは🙅

  ❯ docker pull jenkins/jenkins:2.182-centos
  2.182-centos: Pulling from jenkins/jenkins
  Status: Downloaded newer image for jenkins/jenkins:2.182-centos
  docker.io/jenkins/jenkins:2.182-centos


  # 起動してみる
  ❯ docker run -p 8080:8080 -p 50000:50000 jenkins/jenkins:2.182-centos
  Running from: /usr/share/jenkins/jenkins.war
  webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")

  # warファイルから起動してる??

  # コンテナの中に入る
  ❯ docker run -it --name jenkins-update-test jenkins/jenkins:2.182-centos /bin/bash
  bash-4.2$ cat /etc/redhat-release
  CentOS Linux release 7.6.1810 (Core)
  bash-4.2$ echo $HOME
  /var/jenkins_home

war ファイルからの起動のようなのでこれは🙅

Centos 7 のimageにJenkinsをyumインストールする方法を試す

rpmを探す

Jenkinsインストール

Javaランタイムインストール

[root@b92d081ebab0 /]# yum -y install java-1.8.0-openjdk

Jenkins公式yumリポジトリの追加

[root@b92d081ebab0 /]# curl -o /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo

[root@b92d081ebab0 /]# rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

[root@b92d081ebab0 /]# yum info jenkins
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
 * base: ftp.jaist.ac.jp
 * extras: ftp.jaist.ac.jp
 * updates: ftp.jaist.ac.jp
jenkins                                                                                  | 2.9 kB  00:00:00
jenkins/primary_db                                                                       |  35 kB  00:00:00
Available Packages
Name        : jenkins
Arch        : noarch
Version     : 2.263.1
Release     : 1.1
Size        : 64 M
Repo        : jenkins
Summary     : Jenkins Automation Server
URL         : http://jenkins.io/
License     : MIT/X License, GPL/CDDL, ASL2
Description : Jenkins is an open source automation server which enables developers around the world to reliably
            : automate  their development lifecycle processes of all kinds, including build, document, test,
            : package, stage, deployment, static analysis and many?more.
            :
            : Jenkins is being widely used in areas of Continous Integration, Continuous Delivery, DevOps, and
            : other areas. And it is not only about software, the same automation techniques can be applied in
            : other areas like Hardware Engineering, Embedded Systems, BioTech, etc.
            :
            : For information see https://jenkins.io
            :
            :
            : Authors:
            : --------
            :     Kohsuke Kawaguchi <kk@kohsuke.org>

rpmインストール

[root@b92d081ebab0 tmp]# curl -OL http://mirrors.jenkins-ci.org/redhat-stable/jenkins-2.204.2-1.1.noarch.rpm

[root@b92d081ebab0 tmp]# ls
hsperfdata_root  jenkins-2.204.2-1.1.noarch.rpm  ks-script-DrRL8A  yum.log

[root@b92d081ebab0 tmp]# rpm -ivh jenkins-2.204.2-1.1.noarch.rpm

[root@b92d081ebab0 tmp]# systemctl start jenkins
Failed to get D-Bus connection: Operation not permitted

Failed to get D-Bus connection: Operation not permitted エラー

Dockerのエラーのようなので、以下を参考にして対応

qiita.com

一旦削除してdocker runやり直し 特権モードで起動させる

❯ docker run -d --privileged --name jenkins-test centos:centos7 /sbin/init

❯ docker ps
CONTAINER ID   IMAGE            COMMAND        CREATED          STATUS          PORTS     NAMES
cd5af186cc01   centos:centos7   "/sbin/init"   28 seconds ago   Up 26 seconds             jenkins-test

❯ docker exec -it jenkins-test /bin/bash
[root@cd5af186cc01 /]#

## ~~java install, repository 登録, rpm install~~

[root@cd5af186cc01 tmp]# systemctl start jenkins
Job for jenkins.service failed because the control process exited with error code. See "systemctl status jenkins.service" and "journalctl -xe" for details.
[root@cd5af186cc01 tmp]# systemctl status jenkins.service
● jenkins.service - LSB: Jenkins Automation Server
   Loaded: loaded (/etc/rc.d/init.d/jenkins; bad; vendor preset: disabled)
   Active: failed (Result: exit-code) since Sun 2021-01-03 14:45:56 UTC; 15s ago
     Docs: man:systemd-sysv-generator(8)
  Process: 355 ExecStart=/etc/rc.d/init.d/jenkins start (code=exited, status=1/FAILURE)

Jan 03 14:45:56 cd5af186cc01 systemd[1]: Starting LSB: Jenkins Automation Server...
Jan 03 14:45:56 cd5af186cc01 jenkins[355]: /etc/rc.d/init.d/jenkins: line 59: /etc/init.d/functions: No s...tory
Jan 03 14:45:56 cd5af186cc01 systemd[1]: jenkins.service: control process exited, code=exited status=1
Jan 03 14:45:56 cd5af186cc01 systemd[1]: Failed to start LSB: Jenkins Automation Server.
Jan 03 14:45:56 cd5af186cc01 systemd[1]: Unit jenkins.service entered failed state.
Jan 03 14:45:56 cd5af186cc01 systemd[1]: jenkins.service failed.
Hint: Some lines were ellipsized, use -l to show in full.

/etc/rc.d/init.d/jenkins: line 59: /etc/init.d/functions: No such file or directory エラー

以下と同じエラーだったので、同様の対応をしてみる

qiita.com

[root@cd5af186cc01 tmp]# yum provides */init.d/functions*

[root@cd5af186cc01 tmp]# yum install -y initscripts

[root@cd5af186cc01 tmp]# ls /etc/init.d/functions
/etc/init.d/functions

[root@cd5af186cc01 tmp]# systemctl start jenkins
[root@cd5af186cc01 tmp]# systemctl status jenkins
● jenkins.service - LSB: Jenkins Automation Server
   Loaded: loaded (/etc/rc.d/init.d/jenkins; bad; vendor preset: disabled)
   Active: active (running) since Sun 2021-01-03 15:04:01 UTC; 7s ago
     Docs: man:systemd-sysv-generator(8)
  Process: 464 ExecStart=/etc/rc.d/init.d/jenkins start (code=exited, status=0/SUCCESS)
   CGroup: /docker/cd5af186cc0158a972d74dc63069144faa3849d05296134356de89d37a8f7a04/system.slice/jenkins.service
           └─493 /etc/alternatives/java -Dcom.sun.akuma.Daemon=daemonized -Djava.awt.headless=true -DJENKINS_...

Jan 03 15:04:00 cd5af186cc01 systemd[1]: Starting LSB: Jenkins Automation Server...
Jan 03 15:04:00 cd5af186cc01 runuser[469]: pam_unix(runuser:session): session opened for user jenkins by (uid=0)
Jan 03 15:04:01 cd5af186cc01 runuser[469]: pam_unix(runuser:session): session closed for user jenkins
Jan 03 15:04:01 cd5af186cc01 jenkins[464]: Starting Jenkins [  OK  ]
Jan 03 15:04:01 cd5af186cc01 systemd[1]: Started LSB: Jenkins Automation Server.
Hint: Some lines were ellipsized, use -l to show in full.

起動🙆

Dockerfileを作る

以下を参考に作成

qiita.com

qiita.com

Jenkins updateを試す

  • サービスSTOP → yum install → サービスSTART
[root@localhost jenkins]# systemctl stop jenkins
[root@localhost jenkins]# yum install jenkins
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
 * base: ty1.mirror.newmediaexpress.com
 * extras: ty1.mirror.newmediaexpress.com
 * updates: ty1.mirror.newmediaexpress.com
Resolving Dependencies
--> Running transaction check
---> Package jenkins.noarch 0:2.204.2-1.1 will be updated
---> Package jenkins.noarch 0:2.273-1.1 will be an update
--> Finished Dependency Resolution

Dependencies Resolved

================================================================================================================
 Package                   Arch                     Version                     Repository                 Size
================================================================================================================
Updating:
 jenkins                   noarch                   2.273-1.1                   jenkins                    66 M

Transaction Summary
================================================================================================================
Upgrade  1 Package

Total download size: 66 M
Is this ok [y/d/N]: y
Downloading packages:
Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
jenkins-2.273-1.1.noarch.rpm                                                             |  66 MB  00:00:09
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Updating   : jenkins-2.273-1.1.noarch                                                                     1/2
  Cleanup    : jenkins-2.204.2-1.1.noarch                                                                   2/2
  Verifying  : jenkins-2.273-1.1.noarch                                                                     1/2
  Verifying  : jenkins-2.204.2-1.1.noarch                                                                   2/2

Updated:
  jenkins.noarch 0:2.273-1.1

Complete!
[root@localhost jenkins]#
[root@localhost jenkins]# systemctl start jenkins

workspaceが空だと一瞬で終わる模様

Jenkinsのjob,plugin等のバックアップは

sue445.hatenablog.com

のバックアップスクリプトを利用させてもらっている。 そこのコメントに気になる記述がある。

JenkinsCIををrpmでアップデートすると、$JENKINS_HOMEをchown -Rする糞コマンド入っているので、workspace/以下を綺麗にしておくかexport JENKINS_INSTALL_SKIP_CHOWN=trueしておかないと何時間もupdate終わらんとかになる

実際に更新する際は、 export JENKINS_INSTALL_SKIP_CHOWN=true をしておくのがよさそう

バックアップデータからの復元でハマったところ

Error migrating users

バックアップファイルを解凍してjenkinsディレクトリにコピーすればOK
Docker imageの作成→コンテナ起動→jenkins起動は問題なかった
http://localhost:8080 にアクセスしてみると以下のエラーが表示され、ログイン画面がでない

java.nio.file.DirectoryNotEmptyException: /var/lib/jenkins/users/hoge
    at sun.nio.fs.UnixCopyFile.move(UnixCopyFile.java:498)
    at sun.nio.fs.UnixFileSystemProvider.move(UnixFileSystemProvider.java:262)
    at java.nio.file.Files.move(Files.java:1395)
    at hudson.model.UserIdMigrator.migrateUsers(UserIdMigrator.java:97)
    at hudson.model.UserIdMapper.load(UserIdMapper.java:184)
    at hudson.model.UserIdMapper.init(UserIdMapper.java:75)

users データの読み込みに失敗している?

/var/log/jenkins/jenkins.log も見ながら解消していく

2021-01-15 02:48:09.783+0000 [id=34]    INFO    hudson.model.UserIdMigrator#migrateUsers: Migrating user 'hoge' from 'users/hoge/' to 'users/hoge_6878077609320809469/'
2021-01-15 02:48:09.792+0000 [id=34]    SEVERE  hudson.model.UserIdMapper#load: Error migrating users.
java.nio.file.DirectoryNotEmptyException: /var/lib/jenkins/users/hoge
    at sun.nio.fs.UnixCopyFile.move(UnixCopyFile.java:498)
    at sun.nio.fs.UnixFileSystemProvider.move(UnixFileSystemProvider.java:262)
    at java.nio.file.Files.move(Files.java:1395)
    at hudson.model.UserIdMigrator.migrateUsers(UserIdMigrator.java:97)
    at hudson.model.UserIdMapper.load(UserIdMapper.java:184)
    at hudson.model.UserIdMapper.init(UserIdMapper.java:75)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at hudson.init.TaskMethodFinder.invoke(TaskMethodFinder.java:104)
    at hudson.init.TaskMethodFinder$TaskImpl.run(TaskMethodFinder.java:175)
    at org.jvnet.hudson.reactor.Reactor.runTask(Reactor.java:296)
    at jenkins.model.Jenkins$5.runTask(Jenkins.java:1121)
    at org.jvnet.hudson.reactor.Reactor$2.run(Reactor.java:214)
    at org.jvnet.hudson.reactor.Reactor$Node.run(Reactor.java:117)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
2021-01-15 02:48:09.807+0000 [id=34]    SEVERE  jenkins.InitReactorRunner$1#onTaskFailed: Failed UserIdMapper.init

エラー内容を調べる

jenkins-ci.361315.n4.nabble.com

www.jenkins.io

In this case, rename the directory $JENKINS_HOME/users/admin_6635238516816951048/ to $JENKINS_HOME/users/admin/ while Jenkins is not running. Repeat this for all other user directories listed in the log. Doing all of the above will undo the partial migration. Jenkins will re-run it on the next startup.

対応する

このコメントを参考

stackoverflow.com

service stop → users/users.xmlの削除 or rename → service start → 古いユーザーcredentialのまま使う

users/users.xmlはバックアップデータに存在しなかったので、以下の方法で対応

  • バックアップのusersディレクリ名をusers_tmpに変更
  • 空のusersディレクトリを作成
  • jenkins service startする
    • この時点でJenkinsの画面が表示されるか確認。job等は復元されている状態
    • ログインユーザー情報がないので、ログインはできない
  • jenkins service stopする
    • users_tmpディレクト名をusersに変更
    • jenkins service startする
    • ログインできるか確認

おわりに

Dockerfileからのimage作成、コンテナ起動などはシェルスクリプトを作成した。

Dockerで更新検証環境を作ったことで、pluginを気軽に試したり更新したりできるようになりそう😋

storyboardを使わずに姓名診断アプリを作る

ドットインストールの以下のレッスン

dotinstall.com

を、storyboardを使わずにswiftのみで作ってみる。

storyboardを使わないプロジェクト作成

これ参照

画面に必要な部品

姓名診断画面

f:id:gawao:20150330002724j:plain

診断結果画面

f:id:gawao:20150330002756j:plain

NavigationControllerを設置してタイトルを表示

参考

012 UINavigationControllerの表示 - Swift Docs

コード

AppDelegate.swift 抜粋

var window: UIWindow?
var myNavigaionController: UINavigationController?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: bject]?) -> Bool {

    // ViewControllerを生成
    let myFirstViewController: ViewController = ViewController()
    // Navigation Controllerを生成
    myNavigaionController = UINavigationController(rootViewController: myFirstViewController)
    
    self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
    self.window?.backgroundColor = UIColor.whiteColor()
    // Navigation ControllerをrootViewControllerに設定
    self.window?.rootViewController = myFirstViewController
    self.window?.makeKeyAndVisible()
    
    return true
}

ViewController.swift(姓名診断画面) 抜粋

override func viewDidLoad() {
    super.viewDidLoad()

    // タイトルを設定する
   self.title = "姓名診断"
}

実行結果

f:id:gawao:20150330002913j:plain

ViewControllerの追加と部品の配置

参考

コード

ViewController.swift(姓名診断画面)

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {

    let sendButton: UIButton = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Controllerのタイトルを設定する
        self.title = "姓名診断"
    
        // UITextFieldを作成する
        let name: UITextField = UITextField(frame: CGRectMake(0, 0, 300, 30))
        // 最初に表示する文字を設定
        name.text = "名前を入力"
        // Delegateを設定する
        name.delegate = self
        // 枠を表示する
        name.borderStyle = UITextBorderStyle.RoundedRect
        // UITextFieldの表示する位置を設定
//        name.layer.position = CGPoint(x: self.view.frame.width/2, y: 150)
        name.layer.position = CGPoint(x: 160, y: 150)
        // Viwに追加する
        self.view.addSubview(name)
        
        // 送信ボタンを設定する
        sendButton.frame = CGRectMake(0, 0, 50, 40)
        // 背景色を設定する
        sendButton.backgroundColor = UIColor.lightGrayColor()
        sendButton.showsTouchWhenHighlighted = true
        // 枠を丸くする
        sendButton.layer.masksToBounds = true
        // タイトルを設定する
        sendButton.setTitle("Send", forState: UIControlState.Normal)
        sendButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
        // コーナーの半径を設定する
        sendButton.layer.cornerRadius = 10.0
        // ボタンの位置を指定する
        sendButton.layer.position = CGPoint(x: 340, y: 150)
        // タグを設定する
        sendButton.tag = 1
        // イベントを追加する
        sendButton.addTarget(self, action: "showResult:", forControlEvents: .TouchUpInside)
        // ボタンをViewに追加する
        self.view.addSubview(sendButton)

    }

    /*
        ボタンイベント
    */
    func showResult(sender: UIButton) {
        // 移動先のViewを定義する
        let resultViewController = ResultViewController()
        // ResultViewに移動する
        self.navigationController?.pushViewController(resultViewController, animated: true)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

ResultViewController(診断結果画面)

import UIKit

class ResultViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Controllerのタイトルを設定
        self.title = "診断結果"
        
        // Labelを作成
        let label: UILabel = UILabel(frame: CGRectMake(0, 0, 200, 40))
        // 背景色を付ける
        label.backgroundColor = UIColor.lightGrayColor()
        // 文字を代入
        label.text = "あなたの点数は…"
        // テキストを中央寄せにする
        label.textAlignment = NSTextAlignment.Center
        // ラベルを配置する座標を設定する
        label.layer.position = CGPoint(x: self.view.bounds.width / 2, y: 200)
        // ViewにLabelを追加
        self.view.addSubview(label)
        
        // 診断結果表示Labelを作成
        let resultLabel: UILabel = UILabel(frame: CGRectMake(0, 0, 200, 80))
        // 枠線を付ける
        resultLabel.layer.borderColor = UIColor.redColor().CGColor
        // 枠線の太さ
        resultLabel.layer.borderWidth = 2.0
        // 文字を代入
        resultLabel.text = "100点"
        // テキストを中央寄せ
        resultLabel.textAlignment = NSTextAlignment.Center
        // フォントサイズを大きくして太字に
        resultLabel.font = UIFont.boldSystemFontOfSize(64)
        // ラベルを配置する座標を設定する
        resultLabel.layer.position = CGPoint(x: self.view.bounds.width / 2, y: 300)
        // ViewにLabelを追加
        self.view.addSubview(resultLabel    )
        
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

実行結果

f:id:gawao:20150330003218j:plain f:id:gawao:20150330003235j:plain


ここまでで、

  • Sendボタンをクリックして診断結果画面に遷移
  • 診断結果画面から姓名診断画面に遷移

は出来た

姓名診断のText Fieldで入力した値のチェック・値を診断結果画面に表示・点数を表示

参考

010 UIAlertControllerでアラートを表示 - Swift Docs


ここではまってしまった。。。
segueを使わないで画面間で値を受け渡すやり方がわからずに2時間くらい悩んでいた。
名前受け渡し用の変数を作り、そこに値をセットするようにしたら出来たが、これでいいのか・・

コード

ViewController.swift(姓名診断画面)

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {

    let sendButton: UIButton = UIButton()
    var nameField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Controllerのタイトルを設定する
        self.title = "姓名診断"
    
        // UITextFieldを作成する
        nameField = UITextField(frame: CGRectMake(0, 0, 300, 30))
        // 最初に表示する文字を設定
        nameField.placeholder = "名前を入力"
        // Delegateを設定する
        nameField.delegate = self
        // 枠を表示する
        nameField.borderStyle = UITextBorderStyle.RoundedRect
        // UITextFieldの表示する位置を設定
        nameField.layer.position = CGPoint(x: 160, y: 150)
        // Viwに追加する
        self.view.addSubview(nameField)
        
        // 送信ボタンを設定する
        sendButton.frame = CGRectMake(0, 0, 50, 40)
        // 背景色を設定する
        sendButton.backgroundColor = UIColor.lightGrayColor()
        sendButton.showsTouchWhenHighlighted = true
        // 枠を丸くする
        sendButton.layer.masksToBounds = true
        // タイトルを設定する
        sendButton.setTitle("Send", forState: .Normal)
        sendButton.setTitleColor(UIColor.blackColor(), forState: .Normal)
        // コーナーの半径を設定する
        sendButton.layer.cornerRadius = 10.0
        // ボタンの位置を指定する
        sendButton.layer.position = CGPoint(x: 340, y: 150)
        // タグを設定する
        sendButton.tag = 1
        // イベントを追加する
        sendButton.addTarget(self, action: "showResult:", forControlEvents: .TouchUpInside)
        // ボタンをViewに追加する
        self.view.addSubview(sendButton)

    }

    /*
        ボタンイベント
    */
    func showResult(sender: UIButton) {
        // 空文字チェック
        if nameField.text == "" {
            showAlert()
        } else {
            // 移動先のViewを定義する
            let resultViewController = ResultViewController()
            // ResultViewに移動する
            self.navigationController?.pushViewController(resultViewController, animated: true)
            
            // TODO これで渡せるな・・?
            resultViewController.myName = nameField.text
        }
    }
    /*
        エラーアラートを表示
    */
    func showAlert() {
        // UIAlertControllerを作成する
        let myAlert = UIAlertController(title: "エラー", message: "診断したい名前を入力してください", preferredStyle: .Alert)
        // OKのアクションを作成する
        let myOkAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
        // OKのアクションを追加する
        myAlert.addAction(myOkAction)
        // UIAlertを発動する
        presentViewController(myAlert, animated: true, completion: nil)
        
    }
    
    /*
    改行ボタンが押された際に呼ばれる
    */
    func textFieldShouldReturn(textField: UITextField) -> Bool {
        
        // returnを押すとキーボードが隠れる
        textField.resignFirstResponder()
        
        // sendButtonと同じ処理を
        sendButton.sendActionsForControlEvents(.TouchUpInside)
        return true
    }
    
    /*
        UITextFieldが編集された直後に呼ばれる
    */
    func textFieldDidBeginEditing(textField: UITextField) {
        println("textFieldDidBeginEditing: \(textField.text)")
    }
    
    /*
        UITextFieldが編集終了する直前に呼ばれる
    */
    func textFieldShouldEndEditing(textField: UITextField) -> Bool {
        println("textFieldShouldEndEditing: \(textField.text)")
        return true
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

ResultViewController(診断結果画面)

import UIKit

class ResultViewController: UIViewController {
    
    // 入力された名前を入れる
    var myName: String = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Controllerのタイトルを設定
        self.title = "診断結果"
        
        // Labelを作成
        let label: UILabel = UILabel(frame: CGRectMake(0, 0, 200, 40))
        // 背景色を付ける
        label.backgroundColor = UIColor.lightGrayColor()
        // 名前を代入
        label.text = "\(myName)の点数は…"
        // テキストを中央寄せにする
        label.textAlignment = NSTextAlignment.Center
        // ラベルを配置する座標を設定する
        label.layer.position = CGPoint(x: self.view.bounds.width / 2, y: 200)
        // ViewにLabelを追加
        self.view.addSubview(label)
        
        // 診断結果表示Labelを作成
        let resultLabel: UILabel = UILabel(frame: CGRectMake(0, 0, 200, 80))
        // 枠線を付ける
        resultLabel.layer.borderColor = UIColor.redColor().CGColor
        // 枠線の太さ
        resultLabel.layer.borderWidth = 2.0
        // 点数を表示
        let score = arc4random_uniform(101)
        resultLabel.text = "\(score)点"
        // テキストを中央寄せ
        resultLabel.textAlignment = NSTextAlignment.Center
        // フォントサイズを大きくして太字に
        resultLabel.font = UIFont.boldSystemFontOfSize(64)
        // ラベルを配置する座標を設定する
        resultLabel.layer.position = CGPoint(x: self.view.bounds.width / 2, y: 300)
        // ViewにLabelを追加
        self.view.addSubview(resultLabel)
        
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

f:id:gawao:20150330003821j:plain f:id:gawao:20150330003833j:plain


ここまでで大体機能は出来たので、細かいところを変更

修正点

結果画面から姓名診断画面に戻った時に名前が入ったままなので消す

姓名診断画面を表示した時にテキストフィールドにフォーカスをセットする

参考

UITextField、UITextViewのフォーカス制御 - iOSアプリ開発トピック

ViewController.swift(姓名診断画面)

    /*
        Viewが表示される直前に行う処理
    */
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        // 名前入力テキストフィールドを空にする
        nameField.text = ""
        // 名前入力テキストフィールドにフォーカスする
        nameField.becomeFirstResponder()
    }

キーボートのreturnSendに変える

参考

UITextField/UITextViewの文字入力で覚えておくと便利なこと

ViewController.swift(姓名診断画面)

override func viewDidLoad() {
// 〜省略〜
        // キーボードのReturnキーをSendに変える
        nameField.returnKeyType = UIReturnKeyType.Send
        // Viwに追加する
        self.view.addSubview(nameField)
// 〜省略〜
}

結果画面を表示したときに姓名診断画面の「テキストフィールドとボタン」が少しの間表示されているので消す

こんなやつ

f:id:gawao:20150330004040g:plain

ResultViewController(診断結果画面)

    /*
    Viewが表示される直前に行う処理
    */
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        // TODO viewの背景色を設定
        // これで前の画面の残像(テキストフィールドとボタン)は消えたけど・・・??
        self.view.backgroundColor = UIColor.whiteColor()
    }

f:id:gawao:20150330004114g:plain

残像が消えたようだけどこれでいいのか・・・??

完成

画面

f:id:gawao:20150330004147g:plain

コード

iOSで姓名診断アプリを作る swift版


やりたいことは出来ましたが、何だかスッキリしない(>_<)

storyboardを使わずにiOSでおみくじを作る

ドットインストールの以下のレッスン

dotinstall.com

を、storyboardを使わずにswiftのみで作ってみる。

storyboardを使わないプロジェクト作成

を参考にして作成。ありがとうございます。

  • Single View Applicationを選択してプロジェクト作成
    • プロジェクトの設定ファイルの変更
      • Deployment Info -> Main Interface の中のMain削除
    • Main.storyboardLaunchScreen.xibを削除
    • AppDelegate.swiftapplicationメソッドにコードを追加
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // ====↓追加=====
    self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
    self.window?.backgroundColor = UIColor.whiteColor() // 背景白
    self.window?.rootViewController = ViewController() // ViewControllerを指定
    self.window?.makeKeyAndVisible()
    // ====↑追加====
    return true
}

画面に必要な部品

  • ラベル1(あなたの運勢は・・・)
  • ラベル2(フォント大きめ。おみくじの結果を表示)
  • ボタン1(クリックすると占いを実行し、占い結果をラベルに表示)

ラベルとボタンを配置してみる

参考

001 UILabelで文字を表示 - Swift Docs

007 UIFontで文字を装飾 - Swift Docs

002 UIButtonでボタンを表示 - Swift Docs

感謝。

f:id:gawao:20150327150555j:plain

ボタンをクリックした時の処理を追加

ボタンをクリックした時にコンソールに出力するようにしてみる

// イベントを追加する
divineButton.addTarget(self, action: "onClickDivineButton", forControlEvents: .TouchUpInside)
 

/*
    占うイベント
*/
func onClickDivineButton(sender: UIButton) {
    println("onClickDivineButton: ")
    println("sender.currentTitle: \(sender.currentTitle)")
}

ボタンをクリックしてみるとエラーが(ヽ'ω`)・・・??

MyOmikujiSwiftOnly[12918:4911213] -[MyOmikujiSwiftOnly.ViewController onClickDivineButton]: unrecognized selector sent to instance 0x7fdba25bdeb0
2015-03-27 11:24:26.043 MyOmikujiSwiftOnly[12918:4911213] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyOmikujiSwiftOnly.ViewController onClickDivineButton]: unrecognized selector sent to instance 0x7fdba25bdeb0'

メソッド指定時にコロン:が付いていなかった!!

// メソッド指定にコロンを追加
// イベントを追加する
divineButton.addTarget(self, action: "onClickDivineButton:", forControlEvents: .TouchUpInside)
onClickDivineButton: 
sender.currentTitle: Optional("占った!")

成功!!

適当に装飾

  • Labelの背景色
  • 枠線、角丸、フォントサイズ
  • ボタンの背景色、角丸

f:id:gawao:20150327150958j:plain

完成

f:id:gawao:20150327151053g:plain

ソースコード

はまったところ

  • storyboardを使わずにプロジェクを作る方法がわからなかったので検索していくつか試した。時間かかった
  • 乱数を取得し、その数でenumから値を引っ張ろうとしたがやり方がわからずなかなかうまくいかなかった

storyboard使わないと位置の調整とか大変そう。
使ったら使ったで、複雑になってくると大変なのかな。。。
enumとか上手く使えるようになりたいです。
少しずつ作っていってコツを掴んでいきたい所存です。

Swift入門メモ -ジェネリクス-

24 ジェネリクス

dotinstall.com

  • 抽象化されたデータ型
  • どのような型が来てもOK
    • <T> -> Tは何の型が来てもいいという意味
// ある整数を指定した個数分だけ集めた配列を返す関数
func getIntArray(item: Int, count: Int) -> [Int] {
    var result = [Int]()
    for _ in 0..<count {
        result.append(item)
    }
    return result
}

getIntArray(5, 5) // [5, 5, 5, 5, 5]


/*
  getIntArrayを拡張してString型やFloat型にも対応させたい
    -> そのために使えるのがジェネリクス
*/

// 関数の後に型の名前を書く
// 「T」は何の型が来てもいいという意味
func getArray<T>(item: T, count: Int) -> [T] {
    var result = [T]()
    for _ in 0..<count {
        result.append(item)
    }
    return result
}


// 整数型
getArray(3, 4) // [3, 3, 3, 3]

// String型
getArray("EBC", 8) // ["EBC", "EBC", "EBC", "EBC", "EBC", "EBC", "EBC", "EBC"]

// Float型
getArray(3.1419, 2) // [3.1419, 3.1419]

Javaにもこんなのあったような。。
一先ずドットインストール終了(^ν^)

Swift入門メモ -extension-

23 extensionで機能拡張

dotinstall.com

  • 既存のデータ型、クラス、構造体を拡張する
    • String,Intの基本データ型以外にもクラス・構造体・列挙型にも使える
  • 何らかの機能を追加したいときに使う
extension String {
    var size: Int {
        // 自身の文字数を返す
        return countElements(self)
    }
    func ebc() -> String {
        // 何が何でも"EBC!!!!"しか返さない
        return "EBC!!!!"
    }
}

var s: String = "hoge"
s.size // 4
s.ebc() //EBC!!!!

extension Int {
    func fjhss() -> Int {
        // 自身の数から12を引く(中学何年生?)
        return self - 12
    }
}

var age: Int = 12
age.fjhss() // 0
age = 33
age.fjhss() // 21

Swift入門メモ -構造体-

22 構造体

dotinstall.com

構造体とは

  • クラスとほぼ似た感じでプロパティやメソッドをまとめたデータ型
  • プロトコル、イニシャライザーなども使える
  • 構造体の宣言はstruct

クラスとの違い

  • メソッドの中で自信のプロパティを書き換えることが出来ない
    • 書き換える場合は明示的に指示
    • funcの前にmutating
  • 継承が出来ない
  • コピーするときの挙動が違う
    • クラス:参照渡し
      • メモリを効率的に使うため
    • 構造体:値渡し
      • わかりやすいが、大きな構造体だとコピーするたびにメモリを圧迫するので注意が必要
struct UserStruct {
    var name: String
    var score: Int = 0
    init(name: String) {
        self.name = name
    }
    mutating func upgrade() {
        score++
    }
}


class User {
    var name: String
    var score: Int = 0
    init(name: String) {
        self.name = name
    }
    func upgrade() {
        score++
    }
}

// ====クラスのインスタンスコピー
var hoge = User(name: "Hoge")
var hoge2 = hoge // hoge2にはhogeの実体を指し示す参照先がコピーされる
hoge2.name = "hoge2" // hoge2のnameプロパティ書き換え
hoge.name // hoge2 に変わる

// ※hogeとhoge2は全く同じデータを指し示している


// ====構造体のインスタンスコピー
var huga = UserStruct(name: "Huga")
var huga2 = huga // huga2にはhugaの値をそのまま丸ごとコピーしている
huga2.name = "huga2" // huga2のnameプロパティ書き換え
huga.name // Huga のまま(hugaとhuga2は別物だから)

クラスと構造体の使い分け

  • 継承が必要、大きなデータを扱う -> クラス
  • シンプルなデータ -> 構造体

Swift入門メモ -get/set,willSet/didSet・Optionl Chaining・Type Casting-

19 get/set,willSet/didSet

dotinstall.com

get/set

  • プロパティを動的に計算
  • プロパティの値を取得する時と設定する時に計算式が書ける
class Student {
    var name: String
    var number: Int
    var attendance: Int = 0
    var rate: Int {
        get {
            // selfは省略出来る
            return Int(Float(attendance) / 10.0 * 100)
        }
        set {
            // 渡された値を「newValue」でとることが出来る
            // attendanceの値を書き換える
            attendance = Int(newValue / 10)
        }
    }
    
    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
    func attended() {
        attendance++
    }
}

var mayama = Student(name: "rika", number: 3)
mayama.attendance // 0
mayama.rate // 0
mayama.attendance = 1
mayama.rate // 10
mayama.attendance = 3
mayama.rate // 30


// rateを更新してattendanceの値を変える
mayama.rate = 100
mayama.attendance // 10
mayama.rate = 80
mayama.attendance // 8
  • getしか使わない場合
class Student {
    var name: String
    var number: Int
    var attendance: Int = 0
    var rate: Int { // getだけの場合はreturnを書くだけ
        return Int(Float(attendance) / 10.0 * 100)
    }
    
    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
    func attended() {
        attendance++
    }
}

willSet/didSet

  • プロパティの状態監視
  • 値が変わった時、変わる前と変わった後で何らかの処理を入れることができる
  • プロパティが最初に初期化されるときには実行されない
class Student {
    var name: String
    var number: Int
    var attendance: Int = 0 {
        willSet { // 変わる前にしたい処理
            // 新しい値は「newValue」
            println("willSet: \(attendance) -> \(newValue)")
        }
        didSet { // 変わった後にしたい処理
            // 古い値は「oldValue」
            println("didSet: \(oldValue) -> \(attendance)")
        }
    }
    var rate: Int {
        return Int(Float(attendance) / 10.0 * 100)
    }
    
    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
    func attended() {
        attendance++
    }
}


var mayama = Student(name: "rika", number: 3)
mayama.attendance = 9 // willSet: 0 -> 9 / didSet: 0 -> 9
mayama.attendance = 10 // willSet: 9 -> 10 / didSet: 9 -> 10

20 Optionl Chaining

dotinstall.com

  • クラスなどが複雑に入り組んでいた時に、プロパティやメソッドがあるか安全に確かめる方法
  • 先頭から調べて行って、nilだった場合はそこで評価を止め、安全に終了させてnilを返す
  • 複雑になると、?マークがたくさんついていく
class User {
    var blog: Blog? // Blog型のOptional
}

class Blog {
    var title = "My Blog"
}

var gawao = User()

// ================
// ★blogプロパティが存在する場合は以下のように取れる
// gawao.blog = Blog()
// Unwrapしてtitleを取得
// gawao.blog!.title // My Blog


// ================
// ★blogプロパティがnilの場合 「gawao.blog!.title」でエラーになる
// Optional Chainingを使う
gawao.blog?.title // nil

// ================
// ★よく書くやり方
// titleがある場合はtを表示する
// - nilの場合はエラーなく終了
// - gawao.blogがある場合は「My Blog」と表示される
if let t = gawao.blog?.title {
    println(t)
}

21 Type Casting

dotinstall.com

  • クラスの型をチェックしたり、あるクラスを親クラス・子クラスとして扱うための方法
  • クラスの型チェックはis
  • asを使ってあるクラスを子クラスに変換することをダウンキャストするという
  • どのようなオブジェクトのインスタンスでもいいという特殊なデータ型がある AnyObject
class User {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class AdminUser: User {}

let kashiwagi = User(name: "hinata")
let matsuno = AdminUser(name: "rina")

// クラスの型が違うがエラーにならない
let users: [User] = [kashiwagi, matsuno]

// クラスごとに処理を分けたいとき
// 型チェック、変換に使える-> is / as

for user in users {
    // AdminUserクラスか調べる->is
    if user is AdminUser {
        // UserクラスからAdminUserクラスにダウンキャスト->as
        let u = user as AdminUser
        println(u.name) //  rina
    }
}
    // userをダウンキャスト->うまくいったらブロックの処理。うまくいかなかったらnilを返す
    if let u = user as? AdminUser {
        println(u.name) //  rina
    }

// ====↓の部分は↑のように書き換えることが出来る====

//    // AdminUserクラスか調べる->is
//    if user is AdminUser {
//        // UserクラスからAdminUserクラスにダウンキャスト->as
//        let u = user as AdminUser
//        println(u.name) //  rina
//    }
class User {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class AdminUser: User {}

class SomeUser {}

let kashiwagi = User(name: "hinata")
let matsuno = AdminUser(name: "rina")
let nakayama = SomeUser()


// 継承関係にないインスタンスが含まれているのでエラーになる
// let users: [User] = [kashiwagi, matsuno, nakayama]

// どのようなオブジェクトのインスタンスでもOKなAnyObjectを使う
let users: [AnyObject] = [kashiwagi, matsuno, nakayama]