20210220のiOSに関する記事は8件です。

【Swift】タプル型の便利な使い方(備考録)

導入

Swiftを使っていて、これまでタプル型を自分で実装して使う機会がなかったんですが、
開発していたらタプル型ってこういう時に使えばいいんだという気づきがあったため記事を書きました。

僕はMVPモデルでアプリ開発をしていた時に、タプル型の利便性に気付かされたので、今回はMVPモデルで作ったサンプルアプリを元に解説します。
※あまりに簡素なサンプルアプリであり、Modelは不要なので今回は、ViewとPresenterのみを使用します。

■ MVPモデルについてはこちらの記事が大変参考になりました
https://qiita.com/hicka04/items/25be38a90fdde29c97c2

サンプルアプリについて

下記に画面のキャプチャを載せました。
「計算ボタン」をタップすると、画面上に書かれている足し算を行い、水色の枠の部分に答えを表示します。
TupleApp.png
ありえないくらい簡素なサンプルアプリでございます。

ソースコード

ちなみに、今回やろうとしている処理はcalculate()がViewControllerから引数を受ける形にすればいいのですが、今回はタプル型を使いたいため、あえて引数を渡さないようにしています。
もっと複雑なアプリを作る場合、Presenter内の処理で、ViewControllerから引数として渡された値以外も、ViewControllerから取得して使いたいということがあると思います。
今回はそういったケースを想定しております。

MVPアーキテクチャを採用する場合、PresenterはViewが持っているLabelなどを直接参照したり、更新したりすることなく、PresenterProtocolで定義されたメソッドを通して、値の取得やViewへの更新依頼を行います。

今回は、getAdditionalNumber()を用いて、ViewControllerの値を取得しています。

■タプル型を活用した場合

1つのメソッドでViewControllerの2つの値を取得することが可能です。今回は、足し算に必要な2つの数を1つのメソッドで取得できています。
あとはPresenterで使いたい値を自由に取り出すだけで、簡単に処理できます。
今回は、calculateTuple()を呼び出した場合の処理になります。

■タプル型を使わない場合

逆に、タプル型を使わない場合、各ラベルを取得するのに別々のメソッドを書かないといけないので、PresenterProtocolに定義しないと行けないメソッドが増殖してしまいます。
そのたびにメソッドを増やさないといけないのは手間ですし、無駄なコードを書かないと行けないかなと思います。
今回は、calculate()を呼び出した場合の処理になります。

ViewController.swift
class ViewController: UIViewController {

    @IBOutlet weak var labelNum1: UILabel!
    @IBOutlet weak var labelNum2: UILabel!
    @IBOutlet weak var labelAnswer: UILabel!

    var presenter: Presenter?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        presenter = Presenter(self)
    }

    @IBAction func touchCalculator(_ sender: Any) {
        // タプル型を使わない場合の処理を実行
        // presenter?.calculate()
        // タプル型を使う場合の処理を実行
        presenter?.calculateTuple()
    }
}


extension ViewController: PresenterProtocol {
    func updateAnswerLabel(answer: Int) {
        labelAnswer.text = "\(answer)"
    }

    // タプル型を使わない場合
    func getAdditionalNumber1() -> String {
        return labelNum1.text ?? ""
    }
    func getAdditionalNumber2() -> String {
        return labelNum2.text ?? ""
    }

    // タプル型を使う場合
    func getAdditionalNumber() -> (num1: String, num2: String) {
        return (labelNum1.text ?? "", labelNum2.text ?? "")
    }
}
Presenter.swift
import Foundation

protocol PresenterProtocol: AnyObject {
    func updateAnswerLabel(answer: Int) -> Void

    // タプル型を使わない場合
    func getAdditionalNumber1() -> String
    func getAdditionalNumber2() -> String
    // タプル型を使う場合
    func getAdditionalNumber() -> (num1: String, num2: String)
}

class Presenter {

    weak var view: PresenterProtocol?

    init(_ view: PresenterProtocol) {
        self.view = view
    }

    func calculate() {
        let _num1 = view?.getAdditionalNumber1()
        let _num2 = view?.getAdditionalNumber2()
        guard let num1 = _num1, let num2 = _num2 else {
            return
        }
        let sum = Int(num1)! + Int(num2)!
        view?.updateAnswerLabel(answer: sum)
    }

    func calculateTuple() {
        let tuple = view?.getAdditionalNumber()
        guard let num1 = tuple?.num1, let num2 = tuple?.num2 else {
            return
        }
        let sum = Int(num1)! + Int(num2)!
        view?.updateAnswerLabel(answer: sum)
    }
}

まとめ

今回は、タプル型の使い所について、自分の備考録を兼ねて書いてみました。
サンプルアプリが簡素だったため、メリットが捉えにくかったかもしれませんが、
「似たような属性の値を複数以上(配列にするほど長くもない程度)取得したい場合に、メソッドの戻り値として使えるよ」
ということが伝わっていればいいなと思います。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JenkinsのBuild AgentをMac上に構築する

Jenkinsを使ったiOSアプリのCI/CD環境を構築するために、ビルドエージェントとしてmac miniを登録する機会があったため手順をまとめておきます。

基本的な流れは GitLab CI Runner に近いです。
https://qiita.com/takamii228/items/64fd6879e6acce845980

なお今回利用したJenkinsのバージョンは 2.235.2 です。

mac miniの設定をする

まずmac miniでJenkinsのAgentが動作する準備をします。

JenkinsのAgentを動作させるためにはJavaが必要なのでHomeBrew経由でインストールします。

$ brew install openjdk@11
$ export PATH="/usr/local/opt/openjdk@11/bin:$PATH"
$ java -version   
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment (build 11.0.9+11)
OpenJDK 64-Bit Server VM (build 11.0.9+11, mixed mode)

またJenkins Agentが動作するための作業ディレクトリを切って移動しておきましょう。

$ mkdir ~/jenkins
$ cd ~/jenkins

今回はサンプル用にかんたんなシェルスクリプトを動作させますが、本来はmac miniのagent上で実行したいCI・CDの実行環境も合わせて構築する必要があります。

JenkinsのJNLPのポートを固定する

JenkinsのAgentの通信方法はsshを使うものとJNLPを使うものを選べますが今回はJNLPを使います。
JNLPはJava Network Launch Protocolの略でJenkinsのビルドエージェントを動作させる仕組みとして用いられています。詳細な説明はこちらを参照してください。

https://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/jnlp.html

Jenkinsのデフォルトの設定だとJNLPで使うポート番号はランダムになるようになっています。ランダムだとセキュアでよいのですが、IPやポートに制限を加えている環境での利用では可変だと困るので、適当な値に固定にしておくとよいでしょう。他のポートとぶつからないようにプライベートポートの範囲である49152 ~65535の間から一つ選びましょう。

JNLPで利用するポート番号の固定はJenkinsにログインして管理画面(Manage Jenkins)に移動してグローバルセキュリティ(Configure Global Security)のAgentの設定で指定できます。

合わせてこのポート番号とmac miniからの通信経路の穴あけを確認しておきましょう。

JenkinsのBuild Agentを追加する

次にJenkinsの管理画面でBuild Agentのノード設定を行います。

管理画面(Manage Jenkins)に移動して「ノード管理(Manage Nodes and Clouds)」を選択します。

ノードの管理画面の左のリストのうち「新規ノードの作成(New Node)」を選択して新しいNodeを作成します。ノードの名前を入力し、Permanent AgentにチェックをいれてOKを押します。

次に詳細な設定を入力していきます。

  • ノード名(Name)
    • ノードの名前を入力します
  • 説明(Description)
    • ノードの説明を入力します
  • 同時ビルド数(# of executors)
    • 同一ノード上での同時ビルド数を入力します。iOSは並行でビルドするとDerivedDataの関係でエラーになる可能性があるので1にします
  • リモートFSルート(Remote root directory)
    • mac mini上での絶対パスをします。今回は /Users/${username}/jenkins です
  • ラベル(Labels)
    • Jenkinsfileでnodeとして指定するラベルを定義します
  • 用途(Usage)
    • 指定された場合のみ動作するか共有にするか選べます。今回は指定された場合のみに設定します。
  • 起動方法(Launch method)
    • Launch agent by connection it to the master にします
  • 可用性(Availability)
    • Keep this agent online as much as possible にします
  • ノードプロパティ
    • 特に設定しません

すべて入力したら保存(Save)します。

Jenkins Agentを起動してMasterと接続する

次にmac mini上でagentを起動してJenkins Masterと接続します。

動作させるJenkins Agent用のjarファイルは http://yourserver:port/jnlpJars/agent.jar にアクセスするとダウンロードすることができます。yourserver:portの部分は自身の動作させている環境に合わせて修正してください。

先程作成したノードの作成画面にagent.jarの起動コマンドが記載されているので、それをmac mini上で起動すればよいです。

$ java -jar agent.jar -jnlpUrl http://${yourhostname}:${port}/computer/${jenkins-agent-node-name}/slave-agent.jnlp \
  -secret ${secretToken} -workDir "/Users/xxxxxx/jenkins" 
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir
INFO: Using /Users/xxxxxx/jenkins/remoting as a remoting work directory
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.WorkDirManager setupLogging
INFO: Both error and output logs will be printed to /Users/xxxxxx/jenkins/remoting
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main createEngine
INFO: Setting up agent: ${jenkins-agent-node-name}
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener <init>
INFO: Jenkins agent is running in headless mode.
Feb 18, 2021 8:04:58 PM hudson.remoting.Engine startEngine
INFO: Using Remoting version: 4.3
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir
INFO: Using /Users/xxxxxx/jenkins/remoting as a remoting work directory
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Locating server among [http://${yourhostname}:${port}/]
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver resolve
INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping]
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver isPortVisible
WARNING: Connection refused (Connection refused)
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver resolve
INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping]
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Agent discovery successful
  Agent address: ${yourhostname}
  Agent port:    ${jnlpport}
  Identity:      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Handshaking
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connecting to ${yourhostname}:${jnlpport}
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Trying protocol: JNLP4-connect
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Remote identity confirmed: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connected

上記ログのように Connect が表示されれば起動完了です。

Jenkinsの管理画面からも確認できます。

settings.png

ジョブを動かしてみる

ジョブが正しく設定されていることを確認してみましょう。Jenkinsでパイプラインジョブを作成して、mac mini上のマシン名を表示するだけのジョブ設定をしたJenkinsfileを入れて実行してみます。labekのところはノードの設定のときに入力したラベル名を入れます。

pipeline {
    agent {
        node {
            label '${nodeLabelName}'
        }
    }

    stages {
        stage('build'){
            steps {
                sh 'uname -n'
            }
        }
    }
}

実行ログでmac miniのマシン名が表示されれば設定は完了です。

Started by user xxxxxx
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on jenkins-agent-sample in /Users/xxxx/jenkins/workspace/jenkins-connect-sample
[Pipeline] {
[Pipeline] stage
[Pipeline] { (build)
[Pipeline] sh
+ uname -n
xxxxxxxxx.local
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

以上で設定は完了です。実際にはこのシェルディレクティブの中でCIやCDのスクリプトを実行するようにして、Jenkinsのジョブ設定でトリガーやパラメータ設定をすれば完成です。

おまけ

JenkinsのAgent起動ジョブはコマンドラインでjavaコマンドで起動していました。もしmac miniを再起動したりコネクションが切れたときに再接続したい場合は、都度マシンにアクセスして起動コマンドを打たないといけません。

この課題を解決するために、macOSでデフォルトで利用できるlaunchdという仕組みを活用します。launchdについては公式サイトに例が沢山乗っているのでこちらを参考にします。

https://launchd.info/

今回は起動時に起動のjavaコマンドを実行し、ネットワークが再接続されたりクラッシュしたら再度javaコマンドを実行するようにlauchdを設定してみます。

launchdはplistファイルに設定や実行コマンドを記述します。Jenkins Agentを起動するplistファイルは以下のようになります。コマンドは絶対パスで記入するのがポイントのようです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>local.Jenkins.Agent.launchd</string>
        <key>KeepAlive</key>
        <dict>
            <key>NetworkState</key>
            <true/>
            <key>Crashed</key>
            <true/>
        </dict>
        <key>RunAtLoad</key>
        <true/>
        <key>ProgramArguments</key>
        <array>
            <string>/usr/local/opt/openjdk@11/bin/java</string>
            <string>-jar</string>
            <string>/Users/xxxxxx/jenkins/agent.jar</string>
            <string>-jnlpUrl</string>
            <string>http://${yourhostname}:${port}/computer/${jenkins-agent-node-name}/slave-agent.jnlp</string>
            <string>-secret</string>
            <string>${secretToken}</string>
            <string>-workDir</string>
            <string>"/Users/xxxxxx/jenkins"</string>
        </array>
        <key>StandardOutPath</key>
        <string>/tmp/jenkins.agent.stdout</string>
        <key>StandardErrorPath</key>
        <string>/tmp/jenkins.agent.stderr</string>
    </dict>
</plist>

作成したファイルは ~/Library/LaunchAgents/ 配下に配置してlaunchctlコマンドでloadすると設定されます。

$ launchctl load ~/Library/LaunchAgents/jenkins.agent.settings.plist  

こうしておくとmac miniの起動時に自動でagent.jarを起動してくれます。
再起動してみましょう。

$ ps aux | grep "agent.jar"
xxxxx          969   0.0  2.5  8137224 211276   ??  S     6:51PM   0:19.33 /usr/local/opt/openjdk@11/bin/java -jar /Users/xxxx/jenkins/agent.jar
$ launchctl list | grep "local.Jenkins"
969 -9  local.Jenkins.Agent.Setting

設定通り起動してくれていますね。

クラッシュしたりネットワークが切れたときについては試してませんがおそらく再起動してくれるはずです。もしかしたらjnlpのレイヤでやってくれるのかもですが、まずはしばらく運用してみようと思います。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS Simulatorのスクリーンショットがデフォルトでデスクトップに保存されるけど保存先を変えたい

iOS Simulatorを利用中にキーボードのCommand + Sを押すとスクリーンショットが撮影されディスクトップに保存されます。

  • Altを押しながらCommand + Sを押すとスクリーンショットの保存先を選択できます
    • 次回はCommand + Sだけで指定した保存先に保存されるようです
      • デフォルトの保存先にするか決めるチェックボックスが表示される場合もあるらしい
  • Command + Control + Cを押すとスクリーンショットがクリップボードにコピーされます
    • Excelスクリーンショットエビデンスが高速に作れます

参考
iphone - Take screenshots in the iOS simulator - Stack Overflow

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pod install実行時に`target overrides the OTHER_SWIFT_FLAGS build setting defined`が表示され、追加したライブラリがロードできない

環境

Xcode Version 12.4
MacOS BigSur 11.2.1

発生した問題

プロジェクトのPodfileにSnapKitを追記し、pod installを実行したところ、
Cannot load underlying module for ‘SnapKit'という問題が発生しました。
追加したライブラリのインポートはできるがロードができていない様子。

$ pod install

Analyzing dependencies
Downloading dependencies
Installing SnapKit (5.0.1)
Generating Pods project
Integrating client project
Pod installation complete! There are 12 dependencies from the Podfile and 22 total pods installed.

[!] The `MySampleApp [Debug]` target overrides the `OTHER_SWIFT_FLAGS` build setting defined in `Pods/Target Support Files/Pods-MySampleApp/Pods-MySampleApp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

error.png

解決方法

[!]の案内通り、対象の設定に$(inherited)を追加する。

手順

xcodeproj → TARGETS → Build Settings → 右上の検索窓でother_swiftとかで検索すると下のような項目がでる。
※ 特に設定してない場合は画像と違って空欄になっているはず

koumoku.png

対象スキームの右側の欄をダブルクリックすると下のような画面が開くので、
+アイコンをクリックして$(inherited)を項目に追加
inhe.png

これで設定は完了。
再度pod installするとメッセージが消えて、ライブラリがロードできるようになっていました。

説明

設定ファイルの内容を継承(inherited)して上書きしていく必要があるそうですが、
今回の場合はその継承関係が途中で切れていたため、正常に反映できていなかったようです。
参考: XCodeのBuild Settingsで、値が決まっていくルール -Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリケーションにキーボードショートカットのサポートを追加(UIKitとSwiftUI両方で)

この短い記事では、アプリケーションにキーボードショートカット (keyboardShortcut, UIKeyCommand) 機能を加える方法について説明します。これは、ユーザーがiPadにキーボードを接続した状態でアプリを実行している場合に便利です。

多くの人はiPad用のキーボードカバーを購入しているので、アプリケーションにキーボードショートカットを実装しておくと便利になるでしょう。実装はほんの数分で完了します。

アプリケーションが提供するすべてのショートカットを表示するには、iPadキーボードのコマンド ⌘ キーを長押しします。それにより提供されているショートカット一覧のメニューが表示されます。

SwiftUIのアプリケーションに

修飾キー keyboardShortcut を使うことで、SwiftUIのインタラクティブなUI要素に、ショートカットキーを簡単に追加できます。

public func keyboardShortcut(_ key: KeyEquivalent, modifiers: EventModifiers = .command) -> some View

例えば、ボタンにキーボードショートカットを追加するには:

Button(action: {
    counter += 1
}, label: {
    Text("+1")
})
.keyboardShortcut("a", modifiers: [.command])

修飾キーについて

修飾キーを1つ以上設定することができます。 capsLock, shift, control, option, command, numericPad, and function.

ユーザーは、アクションを実行するために、修飾キーとあなたが定義した文字キーを押す必要があります。

例えば、キーボードショートカットを .keyboardShortcut("a", modifiers: [.command]) に設定した場合、ユーザーはコマンドキーと文字 a キーを同時に押す必要があります。

キーボードショートカットを .keyboardShortcut("r", modifiers: [.command, .option]) に設定した場合、ユーザーはコマンドキー、オプションキー、文字 r キーを同時に押す必要があります。

キーボードのショートカットを追加できるUI要素

ボタンやスイッチ(トグル)にキーボードのショートカットを追加できます。

UIKitのアプリケーションに

UIKitを使ったアプリケーションの場合、UIKeyCommand オブジェクトを作成し、与えられた一連のキーボードショートカットでkeyCommands プロパティをオーバーライドできます。

class ViewController: UIViewController {

    override var keyCommands: [UIKeyCommand]? {
        return [
            .init(title: "新しいメモを作成する", action: #selector(self.actionCreate), input: "n", modifierFlags: [.command])
        ]
    }

    @objc func actionCreate() {
        print("create")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}

タイトル、画像、実行するアクション、修飾フラグなどを指定できます。これがドキュメンテーションに記載されている関数のコードです。

public convenience init(title: String = "", image: UIImage? = nil, action: Selector, input: String, modifierFlags: UIKeyModifierFlags = [], propertyList: Any? = nil, alternates: [UICommandAlternate] = [], discoverabilityTitle: String? = nil, attributes: UIMenuElement.Attributes = [], state: UIMenuElement.State = .off)

:relaxed: Twitter @MszPro

:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面遷移時の値渡しのやり方【Swift 5】

はじめに

この記事では、異なるファイル間で(classを跨いで)値をやりとりする方法を紹介していきます。
また、付随するものとして、segueを使わずに画面遷移を行う方法も紹介していきます。

環境

・macOS catalina
・Xcode version 12.2
・swift 5.3.1

コードを用いた画面遷移

まずは遷移先の情報を取得します。
この際、""内にはStoryBoard IDを、as!以下は、遷移先のcocoa touch ファイルの名前を入力します。
スクリーンショット 2021-02-20 0.27.26.png
なお、StoryBoard IDは写真の赤丸で囲われた部分から入力します。この時、Use StoryBoard IDに忘れずにチェックを入れるようにしてください。

//遷移先の情報の取得
let nextView = storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController



次に、遷移の方法を指定します。(任意)
上の行では、遷移先の画面をどう表示するかを指定することができます。ここでは、.coverVertical, crossDissolveなどの方法を指定することができます。
下の行では、どのようなアニメーションで遷移を行うのかを指定することができます。

nextView.modalTransitionStyle = .crossDissolve   //遷移の仕方の設定
nextView.modalPresentationStyle = .fullScreen    //遷移先の表示方法の設定



最後に、このコードを実行すると遷移が完了します。

self.present(nextView, animated: true, completion: nil)    //遷移の実行


コードでの遷移を行うメリットとしては、segueを使うよりも、画面遷移の際のイベントを実装しやすいことが挙げられます。
したがって、遷移の際に何らかの処理を行う際には、上記のようにコードを用いた方法で行う方が良いです。

最後に、もう一度全体のコードを載せておきます。

let nextView = storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController
nextView.modalTransitionStyle = .crossDissolve
nextView.modalPresentationStyle = .fullScreen
self.present(nextView, animated: true, completion: nil)

遷移先に値を渡す

ここでは、SecondViewController.swiftに、FirstViewController.swiftから値を渡す場合を考えます。

まずは、値の受け手である、SecondViewController.swiftに、以下のように記述します。

//SecondViewController.swift
import UIKit

class SecondViewController: UIViewController{
      override func viewDidLoad() {
          super.viewDidLoad()
      }
      //FirstViewControllerから受け取る値をいれる変数の宣言
      var reciever = ""
}

この際、reciverが、値を受け取る変数です。実際に使うときは、ここに、値を受け取る変数を自由にセットしてください。

続いては、FirstViewController.swiftの中に、SecondViewController.swiftに値を渡す処理を入力していきます。

//FirstViewController.swift
import UIKit

class FirstViewController: UIViewController{
      override func viewDidLoad() {
          super.viewDidLoad()
      }

      //→ここから遷移のコードなので、実際は遷移したいタイミングで実行される場所に書きます (ex. IBActionの処理の中など)
      //遷移先を取得(上記参照)
      let nextView = storyboard?.instantiateViewController(withIdentifier: "secondViewController") as! ViewController
      //""内には、SecondViewControllerのstoryBoard IDを入力します
      nextView.modalTransitionStyle = .crossDissolve
      nextView.modalPresentationStyle = .fullScreen

      //SecondViewController.swift内の変数recieverに文字列"Hello, world!"を代入
      nextView.reciever = "Hello, world!"

      //遷移を実行
      self.present(nextView, animated: true, completion: nil)
}

上記の手順で、異なるファイル間での値渡しは完了です。

注意事項

遷移先(SecondViewController)にIBActionやIBOutlet接続されている値(LabelやTextfieldなど)は遷移時に指定してしまうとエラーが出てしまいます。
そのような時は一旦遷移先(SecondViewController)に一時的に値を受け取る変数を適当に宣言して、そちらに値を渡して、遷移後にSecondViewControllerのViewDidLoadなどでIBActionやIBOutlet接続されている値に代入するようにしてください。

自作アプリの宣伝

私たちは、自作のリマインダーアプリ「タスクリマインダー -TaskReminder 課題管理-」を公開しています。
ダウンロードはこちらから!
https://apple.co/3jJZ1Cyhttps://apple.co/3jJZ1Cy

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面遷移時の値渡しのやり方

はじめに

この記事では、異なるファイル間で(classを跨いで)値をやりとりする方法を紹介していきます。
また、付随するものとして、segueを使わずに画面遷移を行う方法も紹介していきます。

環境

・macOS catalina
・Xcode version 12.2
・swift 5.3.1

コードを用いた画面遷移

まずは遷移先の情報を取得します。
この際、""内にはStoryBoard IDを、as!以下は、遷移先のcocoa touch ファイルの名前を入力します。
スクリーンショット 2021-02-20 0.27.26.png
なお、StoryBoard IDは写真の赤丸で囲われた部分から入力します。この時、Use StoryBoard IDに忘れずにチェックを入れるようにしてください。

//遷移先の情報の取得
let nextView = storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController



次に、遷移の方法を指定します。(任意)
上の行では、遷移先の画面をどう表示するかを指定することができます。ここでは、.coverVertical, crossDissolveなどの方法を指定することができます。
下の行では、どのようなアニメーションで遷移を行うのかを指定することができます。

nextView.modalTransitionStyle = .crossDissolve   //遷移の仕方の設定
nextView.modalPresentationStyle = .fullScreen    //遷移先の表示方法の設定



最後に、このコードを実行すると遷移が完了します。

self.present(nextView, animated: true, completion: nil)    //遷移の実行


コードでの遷移を行うメリットとしては、segueを使うよりも、画面遷移の際のイベントを実装しやすいことが挙げられます。
したがって、遷移の際に何らかの処理を行う際には、上記のようにコードを用いた方法で行う方が良いです。

最後に、もう一度全体のコードを載せておきます。

let nextView = storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController
nextView.modalTransitionStyle = .crossDissolve
nextView.modalPresentationStyle = .fullScreen
self.present(nextView, animated: true, completion: nil)

遷移先に値を渡す

ここでは、SecondViewController.swiftに、FirstViewController.swiftから値を渡す場合を考えます。

まずは、値の受け手である、SecondViewController.swiftに、以下のように記述します。

//SecondViewController.swift
import UIKit

class SecondViewController: UIViewController{
      override func viewDidLoad() {
          super.viewDidLoad()
      }
      //FirstViewControllerから受け取る値をいれる変数の宣言
      var reciever = ""
}

この際、reciverが、値を受け取る変数です。実際に使うときは、ここに、値を受け取る変数を自由にセットしてください。

続いては、FirstViewController.swiftの中に、SecondViewController.swiftに値を渡す処理を入力していきます。

//FirstViewController.swift
import UIKit

class FirstViewController: UIViewController{
      override func viewDidLoad() {
          super.viewDidLoad()
      }
      //遷移先を取得(上記参照)
      let nextView = storyboard?.instantiateViewController(withIdentifier: "secondViewController") as! ViewController
      //""内には、SecondViewControllerのstoryBoard IDを入力します
      nextView.modalTransitionStyle = .crossDissolve
      nextView.modalPresentationStyle = .fullScreen

      //SecondViewController.swift内の変数recieverに文字列"Hello, world!"を代入
      nextView.reciever = "Hello, world!"

      //遷移を実行
      self.present(nextView, animated: true, completion: nil)
}

上記の手順で、異なるファイル間での値渡しは完了です。

自作アプリの宣伝

私たちは、自作アプリ「タスクリマインダー -TaskReminder 課題管理-」を公開しています。
ダウンロードはこちらから!
https://apple.co/3jJZ1Cyhttps://apple.co/3jJZ1Cy

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】API、JSON解析を使う

今回はこちら↓で作ったSwift5Boketeアプリの復習
【iOS14対応】未経験者がiPhoneアプリ開発者になるための全て iOS Boot Camp

61dfbaebcff9d63d53eabc251ec044c4.gif

検索欄にキーワードを入力すると、関連する画像が出てきます。
気に入った画像に適当にネタを入れて決定ボタンを押し、シェアするボタンを押すとスクリーンショットが作られ、Twitter等に投稿することができます。

このアプリでは
①ライブラリの導入方法
②APIの使い方
③JSONの使い方
を学びました。

cocoapodsでライブラリを導入する

ライブラリに関しては動画内でもこちらのサイトを参照されていたので、このサイトを参考にするのが良いかと思います。

今回使用するライブラリは

pod 'SwiftyJSON' => JSONを使用するライブラリ
pod 'Alamofire' => ネットワークを使用するライブラリ
pod 'SDwebImage' => URLで引っ張ってきた画像を高速で使用することができるライブラリ

の3つです。
UIは以下の通り。
スクリーンショット 2021-02-19 12.11.18.jpg
画像上に各パーツの名前(インスタンス名)を書いています。

ViewController↑で導入したライブラリを使用するのと、デバイス内のアルバムを使用するために

ViewController.swift
import Alamofire
import SwiftyJSON
import SDWebImage
import Photos

classの前に追記します。

作成したスクリーンショットをアルバムに保存するためのプログラムを、viewDidLoad内に記述します。

viewController.swift
PHPhotoLibrary.requestAuthorization { (status) in
  switch(status){
    case .authorized: break
    case .denied: break
    case .noDetermined: break
    case .restricted: break
    }
}

APIについて

APIはApplication Program Interfaceの略です。
簡単に言うと、とあるサービスがソフトウェアの一部を公開して、他のソフトウェアに機能を使えるようにできます(多分)
公開元が発行しているAPIキーを使い、発行元が公開している仕様書に従ってプログラムを記述することで、自分のソフトウェアに機能を導入することができます。

今回使用するのがpixabeyと言う無償画像提供サイトのAPI。
このAPIを使用して行いたいのは、アプリ内のSearchTextFieldに入力したキーワードで画像をpixabayから引っ張り出してくることです。
流れとしては、https://pixabay.com/api/?key=[個人のAPIキー]&q=[検索ワード]pixabayのサーバーにリクエストを送ると、json形式でレスポンス(検索結果)が返ってくるので、このデータにjson解析を行って画像を引っ張ってきます。

例えば検索ワードをyellow+flowerとした場合、pixabayのレスポンスは以下のような形で返ってきます。
スクリーンショット 2021-02-19 14.05.10.png

プログラムを書きます。
まず、画像のURLを取得するメソッドを作成します。

ViewController.swift
func getImages(keyword:String) {
  //APIを使う
    let url = "https://pixabay.com/api/?key=[個人のAPIキー]&q=\(keyword)"
    //Alamofireを使ってHTTPリクエストを行う
    AF.request(url, method: get, parameters: nil, encoding: JsonEncoding.default).responseJSON{ (response) in //<= クロージャー
      switch response.result{
        case .success: //<=サーバーからリクエストを正常に受け取れたかどうかで条件分岐
    //JSON形式で返ってきたデータにJSON解析を行う
          let json:JSON = JSON(response.date as Any) //<=ここでデータを取得する
    //必要なデータを取り出し変数に格納する
          var imageString = json["hits"][self.count]["webformatURL"].string //<=hitsの配列内にあるwebformatURLをとってくる
          self.odaiImageView.sd_setImage(with: URL(string: imageString!), completed:nil)

        case .failure:
          print(error)
        }
      }
    }

流れをざっくり説明すると...
検索欄に入力されたキーワードが、APIキーと共にパラメーターになってpixabayに送信されます。
すると、上のようなデータがJSON形式と言う形で返ってきます。
そのデータは辞書型で値が入っています。
その中の、hitsというキーが持っている配列の中に、検索ワードと一致する分だけ同じ数の配列が入っています。
各配列の中のwebformatURLを、ViewControllerodaiImageViewに表示させたい、というわけです。

そして次のお題ボタンをタップすると、次の画像が表示されるようにしたいので、nextOdaiという名前でアクションを作成します。

ViewController.swift
@IBAction func nextOdai(_ sender: Any) {
  count += 1 //<= 冒頭でvar count = 0と宣言しておく
  if searchTextField.text == ""{
    getImage(keyword: "funny")
  } else {
    getImage(keyword: searchTextField.text!)
  }
}

countをインクリメントすることで、次のお題ボタンを押すたびにJSON内の配列を順番に参照することができます。

但し、次へボタンを押しまくって、["hits"]内の配列の数よりcountが多くなってしまうと、参照する値がなくなってしまい、エラーになってしまします。これを避けるために、getImagesメソッド内のlet imageString =....stringの行の次に、以下のプログラムを追記します。

ViewController.swift
if imageString == nil {
  imageString = json["hits"][0]["webformatURL"].string
                    //↓これは画像を表示できるようにするために導入したSDWebImageのメソッドです
  self.odaiImageView.sd_setImage(with: URL(string: imageString!), completed: nil)
} else {
  self.odaiImageView.sd_setImage(with: URL(string: imageString!), completed: nil)
}

次に、?ボタンを押した時の機能を書きます。

ViewController.swift
@IBAction func searchAction(_ sender: Any) {
  self.count = 0
  if searchTextField.text == "" {
    getImages(keyword: "funny")
  } else {
    getImages(keyword: searchTextField.text!)
  }
}

上との違いはというとself.count = 0ですが、searchActionは検索して一番最初のデータを持ってくるので、count = 0、すなわち["hits"][0]を持ってくる必要があるのでこの記述が必要です。(多分)

次に決定ボタンが押された時の機能を書きます。
決定ボタンが押された時の流れとしては、ViewControllerで選択した画像とコメントを次の画面に遷移して渡す必要があります。
あと、画面間で値を受け渡しするためのprepare for segueも一緒に書きます。

ViewController.swift
@IBAction func done(_sender: Any) {
  performSegue(withIdentifier: "next", sender: nil)
}
override func prepare(for segue: UIStoryBoardSegue, sender: Any?) {
  let shareVC = segue.destination as? ShareViewController
  shareVC?.resultImage = odaiImageView.image!
  shareVC?.commentString = commentTextView.text
}

遷移先のプログラムを書きます。

ShareViewController.swift
var resultImage = UIImage()
var commentsString = String()
var screenShotImage = UIImage()

上の2つの値はViewControllerですでに値を受け取っているので、viewDidLoadメソッド内で

ShareViewController.swift
resultImageView.image = resultImage
commentLabel.text = commentString

と記述して遷移先の画面に値を反映します。

次にスクリーンショットの機能を書きます。
これは何も考えず、こういう風に書くんだな、と思っておくのが良さそうです。

ShareViewController.swift
func takeScreenshot(){
 let width = CGFloat(UIScreen.main.bounds.size.width)
 let height = CGFloat(UIScreen.main.bounds.size.height/1.3)
 let size = CGSize(width: width, height: height)

 UIGraphicsBeginImageContextWithOptions(size, false, 0.0)

 self.view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
 screenShotImage = UIGraphicsGetImageFromCurrentImageContext()!
 UIGraphicsEndImageContext()
}

そして、共有ボタンを押したらスクリーンショットが発動して共有できるようにしたいので、↑のメソッドを利用して、

ShareViewController.swift
@IBAction func share(_ sender: Any) {
  //スクリーンショットを撮る
  takeScreenshot()

  let items = [screenShotImage] as [Any]
 //アクティビティビューに乗っけてシェアする
  let activityView = UIActivityViewController(activityItems: items, applicationActivities: nil)
  present(activityView, animated: true, completion: nil)
}

これで一通り完成です。

感想

Ruby on Railsでもいくつかライブラリ使用しましたが、Swiftのもあるんですね(あほ)。
!だったり?だったりself書いたりって細かい違いのところがまだ把握できてないので、今後の学習の中でつかんでいきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む