20201021のSwiftに関する記事は19件です。

SwiftUI 2.0 でカスタムタブビューを作ってみる

SwiftUI2.0で追加されたAPI

SwiftUI 2.0 で追加されたPageTabViewStyleを使ってみたかったので、カスタムタブのようなものを作ってみました。

とりあえずの完成形

こういったものを作っていこうと思います。
999.gif
GitHubはこちらです。
https://github.com/hoshi005/custom-tab

開発環境

  • Xcode 12.1
  • iOS 14.1

事前準備

タブ部分の作成

スクリーンショット 2020-10-21 21.12.23.png

アニメーションgifファイルはこのように名前をつけて配置したので、名前を合わせる形でenumを定義しています。

enum TabItem: String, CaseIterable {
    case piyo
    case pen
    case neko
    case tobipen

    var name: String {
        "\(self.rawValue).gif"
    }
}

タブの一つ一つを表すためのTabItemViewを追加して、以下のように定義しました。

struct TabItemView: View {

    let tabItem: TabItem
    @Binding var selected: TabItem

    var body: some View {
        // SDWebImageSwiftUIのimportが必要.
        AnimatedImage(name: tabItem.name)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40)
            .onTapGesture {
                selected = tabItem // タップしたら自身をselectedに.
            }
    }
}

メインとなるContentViewには、以下のようにタブビューを定義しました。

struct ContentView: View {

    // タブの選択値と初期値.
    @State private var selected: TabItem = .piyo

    var body: some View {

        // タブビュー部分.
        HStack {
            ForEach(TabItem.allCases, id: \.self) { tabItem in
                TabItemView(tabItem: tabItem, selected: $selected)
            }
        }
        .padding(.vertical, 10.0)
        .padding(.horizontal, 20.0)
        .background(Color.white.clipShape(Capsule()))
        .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5)
    }
}

出来上がったのはこちらです。このままだと、選択状態がよくわからないですね。
001.gif

選択状態がわかるように見た目を調整する

選択時/非選択時で見た目を切り替えるため、TabItemViewを以下のように書き換えます。

  • frameを調整
  • paddingを調整
  • offsetを調整
  • タップ時の処理にアニメーションを伴わせる
var body: some View {
   AnimatedImage(name: tabItem.name)
       .resizable()
       .aspectRatio(contentMode: .fit)
       // 選択状態によって、サイズや間隔を調整する.
       .frame(width: tabItem == selected ? 100 : 40)
       .padding(.vertical, tabItem == selected ? -30 : 0)
       .padding(.horizontal, tabItem == selected ? -14 : 16)
       .offset(y: tabItem == selected ? -15 : 0)
       .onTapGesture {
           withAnimation(.spring()) {
               selected = tabItem // タップしたら自身をselectedに.
           }
       }
}

見た目はこのようになります。選択状態が一目でわかるようになりました。
002.gif

背景色の設定と、タブの配置調整

ContentViewの見た目を調整します。

  • 全体をZStackで囲う
  • 最背面にColor("bg").ignoresSafeArea()を配置して背景色とする
  • タブビュー部分をVStackSpacerを利用して画面下部に配置
var body: some View {

    ZStack {

        // 背景色.
        Color("bg").ignoresSafeArea()

        VStack {

            Spacer(minLength: 0)

            // タブビュー部分.
            HStack {
                ForEach(TabItem.allCases, id: \.self) { tabItem in
                    TabItemView(tabItem: tabItem, selected: $selected)
                }
            }
            .padding(.vertical, 10.0)
            .padding(.horizontal, 20.0)
            .background(Color.white.clipShape(Capsule()))
            .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5)
        }
    }
}

見た目はこうなりました。それっぽくなってきましたね。
003.gif

画面をタブで切り替える

タブは用意したので、このタブに連動して画面が切り替わるようにします。

まずはダミーで画面部分を用意します。
適当なので、こちらは好きに作ってもらって良いと思います。

struct HomeView: View {
    var body: some View {
        Text("Home")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.red)
    }
}

struct ListView: View {
    var body: some View {
        Text("List")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.green)
    }
}

struct SearchView: View {
    var body: some View {
        Text("Search")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.blue)
    }
}

struct SettingView: View {
    var body: some View {
        Text("Setting")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.yellow)
    }
}

最後に、これらのViewをTabViewで定義し、カスタムタブと連動するようにします。

  • TabViewの引数にselectedを指定することで、カスタムタブと連動させる
  • PageTabViewStyleを指定することで、横スワイプでの切り替えを可能にする
ZStack {

    // 背景色.
    Color("bg").ignoresSafeArea()

    // メイン画面部分はTabViewで定義.
    TabView(selection: $selected) {
        HomeView()
            .tag(TabItem.piyo)
        ListView()
            .tag(TabItem.pen)
        SearchView()
            .tag(TabItem.neko)
        SettingView()
            .tag(TabItem.tobipen)
    }
    // PageTabスタイルを利用する(インジケータは非表示).
    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

    VStack {
        // 省略.
    }
}

まとめ

タブの定義がずいぶん簡単にできる印象ですが、それ以上に「切り替え用のUI」を簡単に作成できるのは嬉しいですね。
こちらの記事で作った切替ビューでも同じようなことができそうです。もしよかったら試してみてください。

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

【iOS】FloatingPanelを使ってセミモーダルビューを表示する

今回の目的

セミモーダルビュー
FloatingPanel.gif

これを表示してみたいって人向けの話です。

環境

Xcode 12.0.1
Swift5
CocoaPods 1.10.0

環境設定

podファイルに下記を追加してinatall

pod 'FloatingPanel'

本題

では早速作っていこう

Storyboard

今回のゴールは先ほどのgifの通りボタンをタップするとセミモーダルビューが出てくるところ。
まずは2画面用意し、片方はボタンを設置する。(segueは不要)

ViewController      : ボタンを配置した方
SemiModalViewController :表示するモーダル。見やすいようにオレンジにした
スクリーンショット 2020-10-21 20.51.06.png

ViewController

ViewController.swift
import UIKit
import FloatingPanel

class ViewController: UIViewController,FloatingPanelControllerDelegate{
    var floatingPanelController: FloatingPanelController!
    @IBOutlet weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        //ここは普段のボタンの処理
        button.addTarget(self,action: #selector(self.tapButton(_ :)),for: .touchUpInside)
        floatingPanelController = FloatingPanelController()
        self.delegate = SemiModalViewController()
    }

    @objc func tapButton(_ sender: UIButton){
        // セミモーダルビューとなるViewControllerを生成し、contentViewControllerとしてセットする
        let semiModalViewController = self.storyboard?.instantiateViewController(withIdentifier: "fpc") as? SemiModalViewController
        floatingPanelController.set(contentViewController: semiModalViewController)
        // セミモーダルビューを表示する
        floatingPanelController.addPanel(toParent: self, belowView: nil, animated: false)
        floatingPanelController.delegate = self
        floatingPanelController.addPanel(toParent: self, belowView: nil, animated: false)
    }
    //画面を去るときにセミモーダルビューを非表示にする
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // セミモーダルビューを非表示にする
        floatingPanelController.removePanelFromParent(animated: true)
    }

    // カスタマイズしたレイアウトに変更(デフォルトで使用する際は不要)
    func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
        return CustomFloatingPanelLayout()
    }

    //tipの位置に来たときにセミモーダルビューを非表示にする
    func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
        if targetPosition == .tip{
            vc.removePanelFromParent(animated: true)
        }
    }

}

SemiModalViewController

特に記述の必要なし

CustomFloatingPanelLayout

CustomFloatingPanelLayout.swift
import Foundation
import FloatingPanel

class CustomFloatingPanelLayout: FloatingPanelLayout {
    // カスタマイズした高さ
       func insetFor(position: FloatingPanelPosition) -> CGFloat? {
           switch position {
           case .full: return 16.0
           case .half: return 216.0
           case .tip: return 44.0
           default: return nil
           }
       }


   // 初期位置
   var initialPosition: FloatingPanelPosition {
       //half位置から始める
       return .half
   }

   // サポートする位置
   var supportedPositions: Set<FloatingPanelPosition> {
    //full,half.tipの3種類が存在 今回は3種類とも使えるように設定
    return [.full,.half,.tip]
   }
}

まとめ

見事セミモーダルビューを表示することに成功
次はセミモーダルビュー に値渡しする方法を書く

参考

https://qiita.com/dotrikun/items/369f5c0730f444d97cf1

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

Zendesk Chat SDK を使ってiOSアプリにカスタマーサポートチャットを1時間で実装する

はじめに

はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
Chat SDK v2 for iOSを利用しています)

zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。

zendesk chatの説明

このようなチャットの画面を簡単に構築できるツールです。

料金形態は↓こんな感じ
image.png

詳細な料金表はこちら

無料含めて4プランあり、
push通知が$19/月のTeam以上、
営業時間設定が$35/月のProfessional以上になっています。

ある程度規模のあるプロダクトなら Professional プランが安心ですが、工夫次第では無料プランでも十分に活用できそうです。

大まかな手順

  • SDK導入
  • 初期化
  • チャット画面の呼び出し
  • エージェントがオフラインならチャットボタンを消す
  • Push通知設定

SDK導入

公式ドキュメントに従って作業します。

SDKの導入はCocoaPods、Carthage、手動の3パターンありますが
私はCocoaPodsを利用しています。

これがPodfileです

target 'MyApp' do
 # zendesk chatに必要
  pod 'ZendeskChatSDK'

  # zendesk chatで細かい挙動のフックをしたいときに必要
  pod 'ZendeskChatProvidersSDK'

  # push通知を飛ばすために必要だった Support
  pod 'ZendeskSupportSDK'

ハマったのが、Push通知を飛ばすために
どうやらZendeskSupportSDKが必要になるということでした。

「SDKが用意してくれる基本のチャット画面を利用するだけ」という必要最低限の利用であれば
pod 'ZendeskChatSDK'だけで動きます。

初期化

ZendeskSuportとChatを初期化していきます。

import ZendeskCoreSDK
import SupportProvidersSDK
import ChatProvidersSDK
import ChatSDK

Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
Support.initialize(withZendesk: Zendesk.instance)

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

Zendesk.initializeに渡すID群は zendeskの設定 > install modele SDK
記載されているのをコピペするだけなのですが、
ここで迷うのが Chat.initializeに渡す accountKeyappId って
どこに書いてあるんだ!ということです。

accountKeyはこちら、chat > 右上のアイコン > 接続を確認 で確認できます。

appId はメソッドの定義を見ると appId: String? = nil と、
appIdを指定し無くても実行できます。

無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。

appIdは後半の Push通知についての箇所で明記するので
いったん Chat.initialize(accountKey: チャットアカウントキー) だけで動かして
先に進んでしまっても問題ありません。

ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。

// 匿名ユーザーとして作成
let anonymous = Identity.createAnonymous()
Zendesk.instance?.setIdentity(anonymous)

これらを AppDelegate.swiftdidFinishLaunchingWithOptions で初期化します。

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

  Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
  Support.initialize(withZendesk: Zendesk.instance)
  Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

  let anonymous = Identity.createAnonymous()
  Zendesk.instance?.setIdentity(anonymous)
}

わかりやすいようにベタで書いていますが、
当然メソッドに切り出すなりクラスに切り出すなりしてご使用ください。笑

チャット画面の呼び出し

公式ドキュメントだとこの辺りです。

    // 「チャット画面を開くボタン」のタップアクション
    @IBAction func tapStartChat(_ sender: Any) {
        try! startChat()
    }

    // チャット画面への遷移(ViewContollerを生成してモーダル遷移)
    func startChat() throws {
        let chatViewController = trybuildChatUI()

        let button = UIBarButtonItem(title: "閉じる", style: .plain, target: self, action: #selector(dismissNavigationController))
        chatViewController.navigationItem.leftBarButtonItem = button

        let modalController = UINavigationController(rootViewController: chatViewController)
        present(modalController, animated: true)
    }

    // チャット画面の生成
    private func buildChatUI() throws -> UIViewController {
        let messagingConfiguration = getMessagingConfiguration()
        let chatConfiguration = getChatConfigure()

        // Build view controller
        let chatEngine = try ChatEngine.engine()
        return try Messaging.instance.buildUI(engines: [chatEngine], configs: [messagingConfiguration, chatConfiguration])
        // ちなみにenginesにはChatEngineだけでなく、
        // メッセージを残してメールでやり取りをしたりFAQを用意するSupportEngine、
        // Botが自動でよしなに返信してくれるAnswerBotEngineがあります。
    }


    // チャットBotの表示の設定
    private func getMessagingConfiguration() -> MessagingConfiguration {
        let messagingConfiguration = MessagingConfiguration()
        messagingConfiguration.name = "チャット画面に表示されるBotの名前"
        return messagingConfiguration
    }

    // チャットの詳細設定
    private func getChatConfigure() -> ChatConfiguration {
        // isPreChatFormEnabled = trueだと
        // 画面を開いたときに「お問い合わせありがとうございます。」とチャットが自動でメッセージを送信し
        // ユーザーに対して、名前やメールアドレス、電話番号などを質問してくれる
        let chatConfiguration = ChatConfiguration()
        chatConfiguration.isPreChatFormEnabled = true

        // isPreChatでどの情報をユーザーに聞くか
        let formConfiguration = ChatFormConfiguration(name: .optional, email: .optional, phoneNumber: .hidden, department: .hidden)
        chatConfiguration.preChatFormConfiguration = formConfiguration

        return chatConfiguration
    }

    @objc private func dismissNavigationController() {
        self.navigationController?.dismiss(animated: true, completion: nil)
    }

これでチャット画面を開き、
エージェントとチャットをすることができるようになりました。

エージェントがオフラインならチャットボタンを消す

エージェントがオフラインだと
チャットを開始すると自動で 「オンライン中のエージェントがおりません」 とBotが回答してくれます。

とはいえ、この辺の自動回答メッセージがカスタマイズできない(多分)こともあり、
エージェントがいないときはボタン自体をdisabledにしたい!という場合は
ChatProvidersSDKの機能を使って実装していきます。

公式ドキュメントだとこの辺り

Chat.accountProvider?.getAccount { (result) in
            switch result {
            case .success(let account):
                switch account.accountStatus {
                case .online:
                    // オンラインの場合、ボタンをenabledに
                    chatButton.isEnabled = true
                default:
                    // オンライン以外の場合、ボタンをdisabledに
                    chatButton.isEnabled = false
                }
            case .failure(let error):
                // エラーが返ってきたときもボタンをdisabledに
                chatButton.isEnabled = false
            }
        }

Push通知設定

いよいよ Push通知設定です。
Push通知は、エージェントがユーザーに向けて返信をした場合と
エージェントがチャットを終了させた場合に発火します。

  • pemの作成
  • zendesk chatの管理画面にpemをアップロード
  • pem登録時に作成されるアプリIDをChat.initialize()に渡す
  • push通知の実装

この流れでpush通知が利用できるようになります。

pemの作成

参考になるのはこちらの記事です。

ご自身のApple Developerのページから作業します。

push通知の設定のEditをクリックし、

Production SSL Certificate をCreate Certificate します

すると.p12の証明書ファイルがDLできるのでこれからpemを作成します。

先程の公式ドキュメントに

Save the .p12 file to your computer. Leave the password empty.

と書いてあるのでpasswordは空で設定します。

ターミナルの作業

> openssl pkcs12 -clcerts -nodes -in ~/Desktop/証明書.p12  -out myapp.pem                                                                       

Enter Import Password: # 空でEnter
MAC verified OK

pemが正しく生成されたのかテストを実行

> openssl s_client -connect gateway.push.apple.com:2195 -cert myapp.pem -keymyapp.pem

~~~大量の文字列~~~
New, TLSv1/SSLv3, Cipher is DES-CBC3-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : DES-CBC3-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: 
    Start Time: 1602554889
    Timeout   : 7200 (sec)
Verify return code: 0 (ok)

最後が Verify return code: 0 (ok) になっていれば成功です。

zendesk chatの管理画面にpemをアップロード

chat管理画面 > 設定 > アカウント > モバイルSDK > アプリを追加
でpemを登録すると「アプリが作成されました」
とアプリIDが生成されます。

スクリーンショット 2020-10-21 16.57.15.jpg

pem登録時に作成されるアプリIDをChat.initialize()に渡す

最初の初期化のときのこれ↓です

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ここ!!)

これに気が付かず半日潰した。。。

push通知の実装

公式ドキュメントはこちら

一般的にiosアプリでpush通知を利用するときと同様の設定も多く含みます。

・push通知の許可依頼を表示させ

import UserNotifications

let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
   guard granted else { return }

   DispatchQueue.main.async {
      UIApplication.shared.registerForRemoteNotifications()
   }
}

・didRegisterForRemoteNotificationsWithDeviceTokenでトークンを登録

AppDelegate.swift
import ChatProvidersSDK

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Chat.registerPushToken(deviceToken)
}

・ push通知をタップしたときの挙動

AppDelegate.swift
import ChatProvidersSDK

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
   let userInfo = response.notification.request.content.userInfo
   let application = UIApplication.shared
   Chat.didReceiveRemoteNotification(userInfo, in: application)
   completionHandler()
}

・アプリがフォアグラウンドでもpush通知を受信

AppDelegate.swift
import UserNotifications

func userNotificationCenter(_ center: UNUserNotificationCenter,
                           willPresent notification: UNNotification,
                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
   completionHandler([.alert, .sound, .badge])
}

これでエージェントが返信をするとpush通知が届きます!

まとめ

  • チャット画面を開いて、エージェントとチャットができた
  • エージェントのオンライン状態をアプリ側でフックできた
  • チャットが来たときに Push通知を受け取ることができた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

zendesk chat ios SDK でチャットができてPush通知が飛ぶまでの手順

はじめに

はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
Chat SDK v2 for iOSを利用しています)

zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。

zendesk chatの説明

このようなチャットの画面を簡単に構築できるツールです。

料金形態は↓こんな感じ
image.png

詳細な料金表はこちら

無料含めて4プランあり、
push通知が$19/月のTeam以上、
営業時間設定が$35/月のProfessional以上になっています。

ちなみに私は Professional プランを選択しました。

間違えて、返信のできない深夜や土日にフェームをオンライン状態にしてしまったら
ユーザーが困ってしまいますからね。

大まかな手順

  • SDK導入
  • 初期化
  • チャット画面の呼び出し
  • エージェントがオフラインならチャットボタンを消す
  • Push通知設定

SDK導入

SDK v2

公式ドキュメントに従って作業します。

SDKの導入はCocoaPods、Carthage、手動の3パターンありますが
私はCocoaPodsを利用しています。

これがPodfileです

target 'MyApp' do
 # zendesk chatに必要
  pod 'ZendeskChatSDK'

  # zendesk chatで細かい挙動のフックをしたいときに必要
  pod 'ZendeskChatProvidersSDK'

  # push通知を飛ばすために必要だった Support
  pod 'ZendeskSupportSDK'

ハマったのが、Push通知を飛ばすために
どうやらZendeskSupportSDKが必要になるということでした。

「SDKが用意してくれる基本のチャット画面を利用するだけ」という必要最低限の利用であれば
pod 'ZendeskChatSDK'だけで動きます。

初期化

ZendeskSuportとChatを初期化していきます。

import ZendeskCoreSDK
import SupportProvidersSDK
import ChatProvidersSDK
import ChatSDK

Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
Support.initialize(withZendesk: Zendesk.instance)

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

Zendesk.initializeに渡すID群は zendeskの設定 > install modele SDK
記載されているのをコピペするだけなのですが、
ここで迷うのが Chat.initializeに渡す accountKeyappId って
どこに書いてあるんだ!ということです。

accountKeyはこちら、chat > 右上のアイコン > 接続を確認 で確認できます。

appId はメソッドの定義を見ると appId: String? = nil と、
appIdを指定し無くても実行できます。

無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。

appIdは後半の Push通知についての箇所で明記するので
いったん Chat.initialize(accountKey: チャットアカウントキー) だけで動かして
先に進んでしまっても問題ありません。

ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。

// 匿名ユーザーとして作成
let anonymous = Identity.createAnonymous()
Zendesk.instance?.setIdentity(anonymous)

これらを AppDelegate.swiftdidFinishLaunchingWithOptions で初期化します。

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

  Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
  Support.initialize(withZendesk: Zendesk.instance)
  Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

  let anonymous = Identity.createAnonymous()
  Zendesk.instance?.setIdentity(anonymous)
}

わかりやすいようにベタで書いていますが、
当然メソッドに切り出すなりクラスに切り出すなりしてご使用ください。笑

チャット画面の呼び出し

公式ドキュメントだとこの辺りです。

    // 「チャット画面を開くボタン」のタップアクション
    @IBAction func tapStartChat(_ sender: Any) {
        try! startChat()
    }

    // チャット画面への遷移(ViewContollerを生成してモーダル遷移)
    func startChat() throws {
        let chatViewController = trybuildChatUI()

        let button = UIBarButtonItem(title: "閉じる", style: .plain, target: self, action: #selector(dismissNavigationController))
        chatViewController.navigationItem.leftBarButtonItem = button

        let modalController = UINavigationController(rootViewController: chatViewController)
        present(modalController, animated: true)
    }

    // チャット画面の生成
    private func buildChatUI() throws -> UIViewController {
        let messagingConfiguration = getMessagingConfiguration()
        let chatConfiguration = getChatConfigure()

        // Build view controller
        let chatEngine = try ChatEngine.engine()
        return try Messaging.instance.buildUI(engines: [chatEngine], configs: [messagingConfiguration, chatConfiguration])
        // ちなみにenginesにはChatEngineだけでなく、
        // メッセージを残してメールでやり取りをしたりFAQを用意するSupportEngine、
        // Botが自動でよしなに返信してくれるAnswerBotEngineがあります。
    }


    // チャットBotの表示の設定
    private func getMessagingConfiguration() -> MessagingConfiguration {
        let messagingConfiguration = MessagingConfiguration()
        messagingConfiguration.name = "チャット画面に表示されるBotの名前"
        return messagingConfiguration
    }

    // チャットの詳細設定
    private func getChatConfigure() -> ChatConfiguration {
        // isPreChatFormEnabled = trueだと
        // 画面を開いたときに「お問い合わせありがとうございます。」とチャットが自動でメッセージを送信し
        // ユーザーに対して、名前やメールアドレス、電話番号などを質問してくれる
        let chatConfiguration = ChatConfiguration()
        chatConfiguration.isPreChatFormEnabled = true

        // isPreChatでどの情報をユーザーに聞くか
        let formConfiguration = ChatFormConfiguration(name: .optional, email: .optional, phoneNumber: .hidden, department: .hidden)
        chatConfiguration.preChatFormConfiguration = formConfiguration

        return chatConfiguration
    }

    @objc private func dismissNavigationController() {
        self.navigationController?.dismiss(animated: true, completion: nil)
    }

これでチャット画面を開き、
エージェントとチャットをすることができるようになりました。

エージェントがオフラインならチャットボタンを消す

エージェントがオフラインだと
チャットを開始すると自動で 「オンライン中のエージェントがおりません」 とBotが回答してくれます。

とはいえ、この辺の自動回答メッセージがカスタマイズできない(多分)こともあり、
エージェントがいないときはボタン自体をdisabledにしたい!という場合は
ChatProvidersSDKの機能を使って実装していきます。

公式ドキュメントだとこの辺り

Chat.accountProvider?.getAccount { (result) in
            switch result {
            case .success(let account):
                switch account.accountStatus {
                case .online:
                    // オンラインの場合、ボタンをenabledに
                    chatButton.isEnabled = true
                default:
                    // オンライン以外の場合、ボタンをdisabledに
                    chatButton.isEnabled = false
                }
            case .failure(let error):
                // エラーが返ってきたときもボタンをdisabledに
                chatButton.isEnabled = false
            }
        }

Push通知設定

いよいよ Push通知設定です。
Push通知は、エージェントがユーザーに向けて返信をした場合と
エージェントがチャットを終了させた場合に発火します。

  • pemの作成
  • zendesk chatの管理画面にpemをアップロード
  • pem登録時に作成されるアプリIDをChat.initialize()に渡す
  • push通知の実装

この流れでpush通知が利用できるようになります。

pemの作成

参考になるのはこちらの記事です。

ご自身のApple Developerのページから作業します。

push通知の設定のEditをクリックし、

Production SSL Certificate をCreate Certificate します

すると.p12の証明書ファイルがDLできるのでこれからpemを作成します。

先程の公式ドキュメントに

Save the .p12 file to your computer. Leave the password empty.

と書いてあるのでpasswordは空で設定します。

ターミナルの作業

> openssl pkcs12 -clcerts -nodes -in ~/Desktop/証明書.p12  -out myapp.pem                                                                       

Enter Import Password: # 空でEnter
MAC verified OK

pemが正しく生成されたのかテストを実行

> openssl s_client -connect gateway.push.apple.com:2195 -cert myapp.pem -keymyapp.pem

~~~大量の文字列~~~
New, TLSv1/SSLv3, Cipher is DES-CBC3-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : DES-CBC3-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: 
    Start Time: 1602554889
    Timeout   : 7200 (sec)
Verify return code: 0 (ok)

最後が Verify return code: 0 (ok) になっていれば成功です。

zendesk chatの管理画面にpemをアップロード

chat管理画面 > 設定 > アカウント > モバイルSDK > アプリを追加
でpemを登録すると「アプリが作成されました」
とアプリIDが生成されます。

スクリーンショット 2020-10-21 16.57.15.jpg

pem登録時に作成されるアプリIDをChat.initialize()に渡す

最初の初期化のときのこれ↓です

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ここ!!)

これに気が付かず半日潰した。。。

push通知の実装

公式ドキュメントはこちら

一般的にiosアプリでpush通知を利用するときと同様の設定も多く含みます。

・push通知の許可依頼を表示させ

import UserNotifications

let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
   guard granted else { return }

   DispatchQueue.main.async {
      UIApplication.shared.registerForRemoteNotifications()
   }
}

・didRegisterForRemoteNotificationsWithDeviceTokenでトークンを登録

AppDelegate.swift
import ChatProvidersSDK

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Chat.registerPushToken(deviceToken)
}

・ push通知をタップしたときの挙動

AppDelegate.swift
import ChatProvidersSDK

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
   let userInfo = response.notification.request.content.userInfo
   let application = UIApplication.shared
   Chat.didReceiveRemoteNotification(userInfo, in: application)
   completionHandler()
}

・アプリがフォアグラウンドでもpush通知を受信

AppDelegate.swift
import UserNotifications

func userNotificationCenter(_ center: UNUserNotificationCenter,
                           willPresent notification: UNNotification,
                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
   completionHandler([.alert, .sound, .badge])
}

これでエージェントが返信をするとpush通知が届きます!

まとめ

  • チャット画面を開いて、エージェントとチャットができた
  • エージェントのオンライン状態をアプリ側でフックできた
  • チャットが来たときに Push通知を受け取ることができた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Zendesk Chat iOS SDK でチャットができてPush通知が飛ぶまでの手順

はじめに

はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
Chat SDK v2 for iOSを利用しています)

zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。

zendesk chatの説明

このようなチャットの画面を簡単に構築できるツールです。

料金形態は↓こんな感じ
image.png

詳細な料金表はこちら

無料含めて4プランあり、
push通知が$19/月のTeam以上、
営業時間設定が$35/月のProfessional以上になっています。

ちなみに私は Professional プランを選択しました。

間違えて、返信のできない深夜や土日にフェームをオンライン状態にしてしまったら
ユーザーが困ってしまいますからね。

大まかな手順

  • SDK導入
  • 初期化
  • チャット画面の呼び出し
  • エージェントがオフラインならチャットボタンを消す
  • Push通知設定

SDK導入

公式ドキュメントに従って作業します。

SDKの導入はCocoaPods、Carthage、手動の3パターンありますが
私はCocoaPodsを利用しています。

これがPodfileです

target 'MyApp' do
 # zendesk chatに必要
  pod 'ZendeskChatSDK'

  # zendesk chatで細かい挙動のフックをしたいときに必要
  pod 'ZendeskChatProvidersSDK'

  # push通知を飛ばすために必要だった Support
  pod 'ZendeskSupportSDK'

ハマったのが、Push通知を飛ばすために
どうやらZendeskSupportSDKが必要になるということでした。

「SDKが用意してくれる基本のチャット画面を利用するだけ」という必要最低限の利用であれば
pod 'ZendeskChatSDK'だけで動きます。

初期化

ZendeskSuportとChatを初期化していきます。

import ZendeskCoreSDK
import SupportProvidersSDK
import ChatProvidersSDK
import ChatSDK

Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
Support.initialize(withZendesk: Zendesk.instance)

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

Zendesk.initializeに渡すID群は zendeskの設定 > install modele SDK
記載されているのをコピペするだけなのですが、
ここで迷うのが Chat.initializeに渡す accountKeyappId って
どこに書いてあるんだ!ということです。

accountKeyはこちら、chat > 右上のアイコン > 接続を確認 で確認できます。

appId はメソッドの定義を見ると appId: String? = nil と、
appIdを指定し無くても実行できます。

無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。

appIdは後半の Push通知についての箇所で明記するので
いったん Chat.initialize(accountKey: チャットアカウントキー) だけで動かして
先に進んでしまっても問題ありません。

ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。

// 匿名ユーザーとして作成
let anonymous = Identity.createAnonymous()
Zendesk.instance?.setIdentity(anonymous)

これらを AppDelegate.swiftdidFinishLaunchingWithOptions で初期化します。

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

  Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
  Support.initialize(withZendesk: Zendesk.instance)
  Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

  let anonymous = Identity.createAnonymous()
  Zendesk.instance?.setIdentity(anonymous)
}

わかりやすいようにベタで書いていますが、
当然メソッドに切り出すなりクラスに切り出すなりしてご使用ください。笑

チャット画面の呼び出し

公式ドキュメントだとこの辺りです。

    // 「チャット画面を開くボタン」のタップアクション
    @IBAction func tapStartChat(_ sender: Any) {
        try! startChat()
    }

    // チャット画面への遷移(ViewContollerを生成してモーダル遷移)
    func startChat() throws {
        let chatViewController = trybuildChatUI()

        let button = UIBarButtonItem(title: "閉じる", style: .plain, target: self, action: #selector(dismissNavigationController))
        chatViewController.navigationItem.leftBarButtonItem = button

        let modalController = UINavigationController(rootViewController: chatViewController)
        present(modalController, animated: true)
    }

    // チャット画面の生成
    private func buildChatUI() throws -> UIViewController {
        let messagingConfiguration = getMessagingConfiguration()
        let chatConfiguration = getChatConfigure()

        // Build view controller
        let chatEngine = try ChatEngine.engine()
        return try Messaging.instance.buildUI(engines: [chatEngine], configs: [messagingConfiguration, chatConfiguration])
        // ちなみにenginesにはChatEngineだけでなく、
        // メッセージを残してメールでやり取りをしたりFAQを用意するSupportEngine、
        // Botが自動でよしなに返信してくれるAnswerBotEngineがあります。
    }


    // チャットBotの表示の設定
    private func getMessagingConfiguration() -> MessagingConfiguration {
        let messagingConfiguration = MessagingConfiguration()
        messagingConfiguration.name = "チャット画面に表示されるBotの名前"
        return messagingConfiguration
    }

    // チャットの詳細設定
    private func getChatConfigure() -> ChatConfiguration {
        // isPreChatFormEnabled = trueだと
        // 画面を開いたときに「お問い合わせありがとうございます。」とチャットが自動でメッセージを送信し
        // ユーザーに対して、名前やメールアドレス、電話番号などを質問してくれる
        let chatConfiguration = ChatConfiguration()
        chatConfiguration.isPreChatFormEnabled = true

        // isPreChatでどの情報をユーザーに聞くか
        let formConfiguration = ChatFormConfiguration(name: .optional, email: .optional, phoneNumber: .hidden, department: .hidden)
        chatConfiguration.preChatFormConfiguration = formConfiguration

        return chatConfiguration
    }

    @objc private func dismissNavigationController() {
        self.navigationController?.dismiss(animated: true, completion: nil)
    }

これでチャット画面を開き、
エージェントとチャットをすることができるようになりました。

エージェントがオフラインならチャットボタンを消す

エージェントがオフラインだと
チャットを開始すると自動で 「オンライン中のエージェントがおりません」 とBotが回答してくれます。

とはいえ、この辺の自動回答メッセージがカスタマイズできない(多分)こともあり、
エージェントがいないときはボタン自体をdisabledにしたい!という場合は
ChatProvidersSDKの機能を使って実装していきます。

公式ドキュメントだとこの辺り

Chat.accountProvider?.getAccount { (result) in
            switch result {
            case .success(let account):
                switch account.accountStatus {
                case .online:
                    // オンラインの場合、ボタンをenabledに
                    chatButton.isEnabled = true
                default:
                    // オンライン以外の場合、ボタンをdisabledに
                    chatButton.isEnabled = false
                }
            case .failure(let error):
                // エラーが返ってきたときもボタンをdisabledに
                chatButton.isEnabled = false
            }
        }

Push通知設定

いよいよ Push通知設定です。
Push通知は、エージェントがユーザーに向けて返信をした場合と
エージェントがチャットを終了させた場合に発火します。

  • pemの作成
  • zendesk chatの管理画面にpemをアップロード
  • pem登録時に作成されるアプリIDをChat.initialize()に渡す
  • push通知の実装

この流れでpush通知が利用できるようになります。

pemの作成

参考になるのはこちらの記事です。

ご自身のApple Developerのページから作業します。

push通知の設定のEditをクリックし、

Production SSL Certificate をCreate Certificate します

すると.p12の証明書ファイルがDLできるのでこれからpemを作成します。

先程の公式ドキュメントに

Save the .p12 file to your computer. Leave the password empty.

と書いてあるのでpasswordは空で設定します。

ターミナルの作業

> openssl pkcs12 -clcerts -nodes -in ~/Desktop/証明書.p12  -out myapp.pem                                                                       

Enter Import Password: # 空でEnter
MAC verified OK

pemが正しく生成されたのかテストを実行

> openssl s_client -connect gateway.push.apple.com:2195 -cert myapp.pem -keymyapp.pem

~~~大量の文字列~~~
New, TLSv1/SSLv3, Cipher is DES-CBC3-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : DES-CBC3-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: 
    Start Time: 1602554889
    Timeout   : 7200 (sec)
Verify return code: 0 (ok)

最後が Verify return code: 0 (ok) になっていれば成功です。

zendesk chatの管理画面にpemをアップロード

chat管理画面 > 設定 > アカウント > モバイルSDK > アプリを追加
でpemを登録すると「アプリが作成されました」
とアプリIDが生成されます。

スクリーンショット 2020-10-21 16.57.15.jpg

pem登録時に作成されるアプリIDをChat.initialize()に渡す

最初の初期化のときのこれ↓です

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ここ!!)

これに気が付かず半日潰した。。。

push通知の実装

公式ドキュメントはこちら

一般的にiosアプリでpush通知を利用するときと同様の設定も多く含みます。

・push通知の許可依頼を表示させ

import UserNotifications

let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
   guard granted else { return }

   DispatchQueue.main.async {
      UIApplication.shared.registerForRemoteNotifications()
   }
}

・didRegisterForRemoteNotificationsWithDeviceTokenでトークンを登録

AppDelegate.swift
import ChatProvidersSDK

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Chat.registerPushToken(deviceToken)
}

・ push通知をタップしたときの挙動

AppDelegate.swift
import ChatProvidersSDK

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
   let userInfo = response.notification.request.content.userInfo
   let application = UIApplication.shared
   Chat.didReceiveRemoteNotification(userInfo, in: application)
   completionHandler()
}

・アプリがフォアグラウンドでもpush通知を受信

AppDelegate.swift
import UserNotifications

func userNotificationCenter(_ center: UNUserNotificationCenter,
                           willPresent notification: UNNotification,
                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
   completionHandler([.alert, .sound, .badge])
}

これでエージェントが返信をするとpush通知が届きます!

まとめ

  • チャット画面を開いて、エージェントとチャットができた
  • エージェントのオンライン状態をアプリ側でフックできた
  • チャットが来たときに Push通知を受け取ることができた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Zendesk Chat iOS SDK を使ってカスタマーサポートチャットを1時間で実装する

はじめに

はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
Chat SDK v2 for iOSを利用しています)

zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。

zendesk chatの説明

このようなチャットの画面を簡単に構築できるツールです。

料金形態は↓こんな感じ
image.png

詳細な料金表はこちら

無料含めて4プランあり、
push通知が$19/月のTeam以上、
営業時間設定が$35/月のProfessional以上になっています。

ある程度規模のあるプロダクトなら Professional プランが安心ですが、工夫次第では無料プランでも十分に活用できそうです。

大まかな手順

  • SDK導入
  • 初期化
  • チャット画面の呼び出し
  • エージェントがオフラインならチャットボタンを消す
  • Push通知設定

SDK導入

公式ドキュメントに従って作業します。

SDKの導入はCocoaPods、Carthage、手動の3パターンありますが
私はCocoaPodsを利用しています。

これがPodfileです

target 'MyApp' do
 # zendesk chatに必要
  pod 'ZendeskChatSDK'

  # zendesk chatで細かい挙動のフックをしたいときに必要
  pod 'ZendeskChatProvidersSDK'

  # push通知を飛ばすために必要だった Support
  pod 'ZendeskSupportSDK'

ハマったのが、Push通知を飛ばすために
どうやらZendeskSupportSDKが必要になるということでした。

「SDKが用意してくれる基本のチャット画面を利用するだけ」という必要最低限の利用であれば
pod 'ZendeskChatSDK'だけで動きます。

初期化

ZendeskSuportとChatを初期化していきます。

import ZendeskCoreSDK
import SupportProvidersSDK
import ChatProvidersSDK
import ChatSDK

Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
Support.initialize(withZendesk: Zendesk.instance)

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

Zendesk.initializeに渡すID群は zendeskの設定 > install modele SDK
記載されているのをコピペするだけなのですが、
ここで迷うのが Chat.initializeに渡す accountKeyappId って
どこに書いてあるんだ!ということです。

accountKeyはこちら、chat > 右上のアイコン > 接続を確認 で確認できます。

appId はメソッドの定義を見ると appId: String? = nil と、
appIdを指定し無くても実行できます。

無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。

appIdは後半の Push通知についての箇所で明記するので
いったん Chat.initialize(accountKey: チャットアカウントキー) だけで動かして
先に進んでしまっても問題ありません。

ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。

// 匿名ユーザーとして作成
let anonymous = Identity.createAnonymous()
Zendesk.instance?.setIdentity(anonymous)

これらを AppDelegate.swiftdidFinishLaunchingWithOptions で初期化します。

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

  Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
  Support.initialize(withZendesk: Zendesk.instance)
  Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

  let anonymous = Identity.createAnonymous()
  Zendesk.instance?.setIdentity(anonymous)
}

わかりやすいようにベタで書いていますが、
当然メソッドに切り出すなりクラスに切り出すなりしてご使用ください。笑

チャット画面の呼び出し

公式ドキュメントだとこの辺りです。

    // 「チャット画面を開くボタン」のタップアクション
    @IBAction func tapStartChat(_ sender: Any) {
        try! startChat()
    }

    // チャット画面への遷移(ViewContollerを生成してモーダル遷移)
    func startChat() throws {
        let chatViewController = trybuildChatUI()

        let button = UIBarButtonItem(title: "閉じる", style: .plain, target: self, action: #selector(dismissNavigationController))
        chatViewController.navigationItem.leftBarButtonItem = button

        let modalController = UINavigationController(rootViewController: chatViewController)
        present(modalController, animated: true)
    }

    // チャット画面の生成
    private func buildChatUI() throws -> UIViewController {
        let messagingConfiguration = getMessagingConfiguration()
        let chatConfiguration = getChatConfigure()

        // Build view controller
        let chatEngine = try ChatEngine.engine()
        return try Messaging.instance.buildUI(engines: [chatEngine], configs: [messagingConfiguration, chatConfiguration])
        // ちなみにenginesにはChatEngineだけでなく、
        // メッセージを残してメールでやり取りをしたりFAQを用意するSupportEngine、
        // Botが自動でよしなに返信してくれるAnswerBotEngineがあります。
    }


    // チャットBotの表示の設定
    private func getMessagingConfiguration() -> MessagingConfiguration {
        let messagingConfiguration = MessagingConfiguration()
        messagingConfiguration.name = "チャット画面に表示されるBotの名前"
        return messagingConfiguration
    }

    // チャットの詳細設定
    private func getChatConfigure() -> ChatConfiguration {
        // isPreChatFormEnabled = trueだと
        // 画面を開いたときに「お問い合わせありがとうございます。」とチャットが自動でメッセージを送信し
        // ユーザーに対して、名前やメールアドレス、電話番号などを質問してくれる
        let chatConfiguration = ChatConfiguration()
        chatConfiguration.isPreChatFormEnabled = true

        // isPreChatでどの情報をユーザーに聞くか
        let formConfiguration = ChatFormConfiguration(name: .optional, email: .optional, phoneNumber: .hidden, department: .hidden)
        chatConfiguration.preChatFormConfiguration = formConfiguration

        return chatConfiguration
    }

    @objc private func dismissNavigationController() {
        self.navigationController?.dismiss(animated: true, completion: nil)
    }

これでチャット画面を開き、
エージェントとチャットをすることができるようになりました。

エージェントがオフラインならチャットボタンを消す

エージェントがオフラインだと
チャットを開始すると自動で 「オンライン中のエージェントがおりません」 とBotが回答してくれます。

とはいえ、この辺の自動回答メッセージがカスタマイズできない(多分)こともあり、
エージェントがいないときはボタン自体をdisabledにしたい!という場合は
ChatProvidersSDKの機能を使って実装していきます。

公式ドキュメントだとこの辺り

Chat.accountProvider?.getAccount { (result) in
            switch result {
            case .success(let account):
                switch account.accountStatus {
                case .online:
                    // オンラインの場合、ボタンをenabledに
                    chatButton.isEnabled = true
                default:
                    // オンライン以外の場合、ボタンをdisabledに
                    chatButton.isEnabled = false
                }
            case .failure(let error):
                // エラーが返ってきたときもボタンをdisabledに
                chatButton.isEnabled = false
            }
        }

Push通知設定

いよいよ Push通知設定です。
Push通知は、エージェントがユーザーに向けて返信をした場合と
エージェントがチャットを終了させた場合に発火します。

  • pemの作成
  • zendesk chatの管理画面にpemをアップロード
  • pem登録時に作成されるアプリIDをChat.initialize()に渡す
  • push通知の実装

この流れでpush通知が利用できるようになります。

pemの作成

参考になるのはこちらの記事です。

ご自身のApple Developerのページから作業します。

push通知の設定のEditをクリックし、

Production SSL Certificate をCreate Certificate します

すると.p12の証明書ファイルがDLできるのでこれからpemを作成します。

先程の公式ドキュメントに

Save the .p12 file to your computer. Leave the password empty.

と書いてあるのでpasswordは空で設定します。

ターミナルの作業

> openssl pkcs12 -clcerts -nodes -in ~/Desktop/証明書.p12  -out myapp.pem                                                                       

Enter Import Password: # 空でEnter
MAC verified OK

pemが正しく生成されたのかテストを実行

> openssl s_client -connect gateway.push.apple.com:2195 -cert myapp.pem -keymyapp.pem

~~~大量の文字列~~~
New, TLSv1/SSLv3, Cipher is DES-CBC3-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : DES-CBC3-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: 
    Start Time: 1602554889
    Timeout   : 7200 (sec)
Verify return code: 0 (ok)

最後が Verify return code: 0 (ok) になっていれば成功です。

zendesk chatの管理画面にpemをアップロード

chat管理画面 > 設定 > アカウント > モバイルSDK > アプリを追加
でpemを登録すると「アプリが作成されました」
とアプリIDが生成されます。

スクリーンショット 2020-10-21 16.57.15.jpg

pem登録時に作成されるアプリIDをChat.initialize()に渡す

最初の初期化のときのこれ↓です

Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ここ!!)

これに気が付かず半日潰した。。。

push通知の実装

公式ドキュメントはこちら

一般的にiosアプリでpush通知を利用するときと同様の設定も多く含みます。

・push通知の許可依頼を表示させ

import UserNotifications

let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
   guard granted else { return }

   DispatchQueue.main.async {
      UIApplication.shared.registerForRemoteNotifications()
   }
}

・didRegisterForRemoteNotificationsWithDeviceTokenでトークンを登録

AppDelegate.swift
import ChatProvidersSDK

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Chat.registerPushToken(deviceToken)
}

・ push通知をタップしたときの挙動

AppDelegate.swift
import ChatProvidersSDK

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
   let userInfo = response.notification.request.content.userInfo
   let application = UIApplication.shared
   Chat.didReceiveRemoteNotification(userInfo, in: application)
   completionHandler()
}

・アプリがフォアグラウンドでもpush通知を受信

AppDelegate.swift
import UserNotifications

func userNotificationCenter(_ center: UNUserNotificationCenter,
                           willPresent notification: UNNotification,
                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
   completionHandler([.alert, .sound, .badge])
}

これでエージェントが返信をするとpush通知が届きます!

まとめ

  • チャット画面を開いて、エージェントとチャットができた
  • エージェントのオンライン状態をアプリ側でフックできた
  • チャットが来たときに Push通知を受け取ることができた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユニットテストを実践して感じた、初心者がiOSアプリ開発で陥りやすい 3つの悩みとその分析

はじめに

こんにちは、iOSエンジニアの dayossi です。

家族が幸せになれるサービスを提供したいと思って、
HaloHaloという家族日記アプリをリリースしています。

今回は、ユニットテストを通じてアプリ開発をして感じた
3つの悩みについて分析しました。

今回の成果物はコチラです。
https://github.com/Kohei312/TDD_Practice_Poker

これまでよく陥っていた悩み

テストを書かずに開発していた時は、
以下の3つの悩みを切り分けられず、よく悩まされていました。

・UIKitを中心とした、View構築のロジックがおかしいのか...?
・UIを表示するためのデータがおかしいのか...?
・データの受け渡しがおかしいのか...?

ユニットテストを書くことで
このあたりの切り分けがスムーズに行うことができ、
問題を分析しやすくなりました。

問題を分析する際に、以下の3つの視点が
共通して有効だったと感じました。

1. ちゃんと責務を分離できているか?

オブジェクト指向の原則であるSOLID原則の1つ 単一責務の原則 と、
設計方針・意図を意識できているかを確認する問いかけです。

今回は「レイヤー、処理ごとに目的を明確にする」というニュアンスで使用しました。

2. 状態の変化を可視化できているか?

 
1つ目のポイントと重複する部分がありますが、
どの処理を、どのタイミングで呼び、どんな変化が起こっているのか明確にすることです。

特に引数が多くなって、複数の処理を同時に行おうとすると混乱しやすく
それゆえコードミスも起こりやすいと感じました。

enumで状態を列挙しておいたり、特定のレイヤーでのみプロパティの変更を行う処理を定義しておくと、
状態変化を把握しやすかったです。

3. 適度にレイヤーを分割できているか?

ここも1つ目のポイントと重複しますが、
全体設計どおりにレイヤーが機能しているかを逐次確認しました。

各レイヤーがなんの状態を管理するのか、目的を確認しながら
徐々に責務を分離していくと、見直しも行いやすかったです。

以下、今回のアプリ開発で実際に取り組んだ事例を取り上げていきます。

事例:状態が共有されない

今回作成したポーカーゲームのなかで、
プレイヤーは3回までカードを交換できるルールを設けました。
(プレイヤーは、ユーザーとCPUの2人という構成です)

その中で、各プレイヤーがカードを交換した回数に応じて
ゲームの状態が変化する、という点でつまづきが起きました。

交換回数を changeCount という変数で
Playerというプレーヤーの状態を持つ構造体のプロパティとして保持し、
ルールに沿って制御するようにしていました。

PlayerTypeというenumにて、プレーヤーがユーザーかコンピュータなのか
判別するようにしています。

Player.swift
public enum PlayerType{
    case me
    case other
}
struct Player{

    var playerType:PlayerType

    init(playerType:PlayerType){
        self.playerType = playerType
    }

    var playerStatement:PlayerStatement = .thinking
    var changeCount = 3
}

最初はゲーム全体のロジックを管理する PokerInteractor という上位レイヤーに
直接Player型のインスタンスを置いて管理していました。

PokerInteractor.swift
// MARK:- 各プレイヤーのカード交換回数、状態を管理
public struct PokerInteractor{

    var player_me:Player
    var player_other:Player

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

ビジネスロジックを取りまとめるレイヤーとして位置付けており、
ここでプレイヤーの状態と、ゲームの進行状態をコントロールしていました。

ですが、ここに落とし穴がありました。

片方のプレイヤーのターンでは、そのプレイヤーのカード交換回数はちゃんとカウントされていましたが
もう一方のプレイヤーのカード交換回数が共有されないのです。

player_meのターンで、player_meのchangeCountは確かに減っているのに
player_otherのターンになると、player_meのchangeCountが初期値に戻っているのです.

PokerInteractor.swift
public struct PokerInteractor{

#WARNING("いつまでも、状態が共有されない...")
    var player_me:Player
    var player_other:Player

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

問題点:ロジックデータの管理に問題がある

テストでは、実際に計算できていることは確認できており
View側での構築エラーもみられなかったので、ロジックデータの管理に問題があると考えました。

分析:メモリポインタの変更を考慮できていなかった

changeCountはイミュータブルな値なので
値を変更する際は、生成したPlayer型のインスタンスから変更を指示する必要があります。

ただ、値型であるPlayerのプロパティを更新すると、Player全体の値が更新され
Playerをプロパティにもつ上位レイヤーのPokerInteractorも更新されます。

そのため、PokerInteractorで管理していた 2つのPlayerは
結果的に新しいインスタンスが再生成されることとなり、

カードの交換カウントが毎回初期値へリセットされてしまって
プレイヤー全員の状態を共有することができなくなっていました。

そこで、Player全員の状態を把握するための参照型PlayerStatusを一つ追加し
Playerの状態を共有できるように変更して対応しました。

対策:PlayerとPokerInteractorの間に、参照型レイヤーを追加した

PlayerStatusを参照するメモリ領域は常に同一であるため
各プレイヤーの値が更新され、メモリポインタが変更されても
常に変更後の値をスコープできることを狙いとしました。

PlayerStatus.swift
final class PlayerStatus{    

    var players:[Player] = []
    var interactorInputProtocol:InteractorInputProtocol?

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

    func decrementChangeCount(_ playerType:PlayerType){

        self[playerType].changeCount -= 1
        interactorInputProtocol?.checkGameStatement(playerType)

    }
}

配列内にPlayerクラスを格納し、subscriptで必要なプロパティを抽出できるようにしましたが
計算コストが高い上にネストが深く読みづらいので、

今回のアプリのように登場人物が限られるケースでは、
それぞれインスタンスを分けて保持したほうが
わかりやすくて良かったかなと思います。

注意点として、どこからでもPlayerStatusのプロパティを変更し
その状態を共有できてしまうため
計算処理も、PlayerStatusから行うように統一しました。

考察:各モジュールの責務分離があいまいだった

以上で、各プレーヤーの状態管理をPlayerStatusが担うことも明確にし
PokerInteractorからは、状態変更を指示するだけにしました。

言い方を変えれば、ビジネスロジックを管理するPokerInteractorでの
責務は分散できる余地があったことを見逃していたといえます。

テストを通してビジネスロジックの一つ一つの処理は
ちゃんと動いていることを確認できていたため、

PokerInteractorレイヤーの責務が複雑になっていることに
気づくことができたと思います。

まとめ

自分が体験した中で、よく陥りやすい3パターンを抽出しましたが
ホントに基本的なことばかりでお恥ずかしい限りです。

原則を外れてしんどかった部分が多くを占めていることを、改めて実感できました。
もっと上手に設計原則を生かせるように精進します。

温かいツッコミを、お待ちしております。

参考図書・記事

TDD Boot Camp.TDDBC仙台07課題:ポーカー

松尾 和昭,細沼 祐介,田中 賢治 他.iOSテスト全書(2019).PEAKS出版.

関 義隆,史 翔新,田中 賢治 他.iOSアプリ設計パターン入門(2019).PEAKS出版.

田中 賢治. Swiftで書いておぼえるTDD(2018).株式会社インプレス R&D.

[キーワード]
TDD駆動設計:
Dan Chaput, Lee Lambert, Rich Southwell. What is an Enterprise Business Rule Repository?. MODERA analyst.com.
Value Semantics:
Yuta Koshizawa. Value Semantics とは. Heart of Swift
Yuta Koshizawa. Value Semantics を持たない型の問題と対処法. Heart of Swift
Yuta Koshizawa. Swift が値型中心の言語になれた理由とその使い方. Heart of Swift
Copy-on-Write:
(SwiftにおいてCopy-on-Writeは問題にならないと思う)https://qiita.com/koher/items/8c22e010ad484d2cd321
https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd[https://qiita.com/peka2/items/4562456b11163b82feee]

VIPER:
(VIPERアーキテクチャでプロダクトのiOSアプリを1から作ったまとめ)[https://qiita.com/hirothings/items/8ce3ca69efca03bbef88]

SOLID分析:
(SwiftでわかるSOLID原則 iOSDC 2020)https://speakerdeck.com/k_koheyi/swifttewakarusolidyuan-ze-iosdc-2020[https://zenn.dev/k_koheyi/articles/019b6a87bc3ad15895fb]

メモリ:
(Swiftのメモリレイアウトを調べる)
https://qiita.com/omochimetaru/items/64b073c5d6bcf1bbbf99
https://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052
[https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html#//apple_ref/doc/uid/TP40014097-CH46-ID571]

値型:
ミュータブルな型とイミュータブルな型の相違を知ろう
純粋値型Swift

subscript:
Swift の Subscript について

protocol指向:
WWDC 2015 Swiftで値型でより良いアプリを作る

enum:
Swiftの列挙型(enum)おさらい
[Swift] enumはprotocolに準拠できるので、例えばComparableによってシンプルに比較できる

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

NavigationControllerのBackをコードから

一つ前のViewControllerにもどる

self.navigationController?.popViewController(animated: true)

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

Xcode、AppCodeで自動生成されるコメントを変更

概要

Xcodeでファイルを新規に作成すると、以下のようなコメントがヘッダーに追加される。
本名が含まれる場合があるので、これを変更したい。

//
//  ContentView.swift
//  Shared
//
//  Created by [名前] on 2020/10/21.
//

Xcode

以下にテンプレートのファイルのパスが複数示されている。
https://help.apple.com/xcode/mac/9.0/index.html?localePath=en.lproj#/dev91a7a31fc

この中の
User Xcode data: ~/Library/Developer/Xcode/UserData/IDETemplateMacros.plist
にファイルを設置すればよい。

1.ファイルを開く。

vim ~/Library/Developer/Xcode/UserData/IDETemplateMacros.plist

2.デフォルトのテンプレートは以下なので、これをコピペする。

<?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>FILEHEADER</key>
    <string>
//  ___FILENAME___
//  ___TARGETNAME___
//  
//  Created by ___USERNAME___ on ___DATE___
//  ___COPYRIGHT___
//</string>
</dict>
</plist>

3.テンプレートを編集する。
名前を消したいなら「_USERNAME_」を消せばよい。
他の項目を表示する場合、以下の変数を参考にする。
https://help.apple.com/xcode/mac/9.0/index.html?localePath=en.lproj#/dev7fe737ce0

これで完了。
Xcode上でファイルを新規作成すると、テンプレートに指定した通りのコメントが挿入される。

AppCode

上記のXcodeと同様の手順では変更されない。
AppCodeの場合、以下の設定を編集する。
スクリーンショット 2020-10-21 12.46.33.png

・ファイルテンプレートについての説明
https://pleiades.io/help/objc/using-file-and-code-templates.html

・変数の一覧
https://pleiades.io/help/objc/file-template-variables.html

Copyright~~の文は組織名($ORGANIZATION_NAME)が定義されていないと表示されない。
設定するには「⌘;」で設定を開き、GeneralタブからOrganizationを編集する。

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

大学生が研究室配属選考での自己アピールのためにGitHubで製作物を公開した話

経緯

大学の情報系学科に通っている大学3年生です!
私の通っている大学・学科では、3年生の後半(10~11月)に研究室配属があります。

この研究室配属では、各研究室が成績+面接等によって希望者の中から配属者を選考するのですが、面接では研究への興味・プログラミング能力・継続力・学習意欲などの自己アピールを求められます。そして、ここで重要なのは自己アピールにはGitHub等にあげた製作物も利用することができるという点です。

今回、私も研究室配属選考に備え、自己アピールに使うために過去の製作物をGitHubで公開し、「せっかく公開するんだったら記事でも書いてみようかな」と思って記事を書いて見ました。

製作物

Qiita_for_iOS

iOS開発の学習のために作成した俺得Qiita閲覧アプリです。

image1.png image2.png image3.png image4.png image5.png
  • 記事の閲覧・検索やLGTM・ストック、ストック記事の確認などができます。
  • Qiitaの公開APIを使用
  • iOS13のキャッチアップを兼ねて作成したので、UICollectionViewCompositionalLayoutProperty Wrapperなどを盛り込んでいます。

discord_clone_firebase

React開発の学習のために作成したチャットサービスDiscordのクローンアプリです。

デモ: https://discord-clone-36c89.web.app/
※ ユーザー名の入力が求められますが、「test」等を入力していただければ大丈夫です。

image1.png

  • メッセージ送信, 画像・ファイル送信, AddReactionなどができます
  • バックエンドにFirebaseを利用
  • React + Typescript + React Hooks

大学2年生以下の開発者様へ

製作物があると自己アピールに使える武器が手に入るので、作っておいて損はないと思いますし、研究室配属選考に限らず、その自己アピールが使えるケースも多いのではないでしょうか。

私の場合は製作物がiOS, Reactアプリケーションなので研究内容に直結しにくいですが、それでもプログラミング能力・継続力・学習意欲などのアピールになると思いますし、更に近年は機械学習・画像処理・音声認識などの比較的研究に関連しやすい分野も個人開発で手を出せるので、製作物で研究への興味や経験をアピールすることも可能だと思います。

もし、大学2年生以下で開発をしているのであれば、1つで良いのである程度形になった・公開できる製作物を作っておくと研究室配属で役に立つかもしれません!!

まとめ

(大学の先輩から聞いた話なのですが)
研究室配属選考の面接だと、「〇〇に興味があります。」 「プログラミングは得意です。」等の口頭での自己アピールのみの学生も多いらしく、その中で「〇〇に興味があります。〇〇を作りました。ソースコードはGitHubに上げてあります。」 「プログラミングは得意です。〇〇を作りました。△△にリリースしてあります。」と言った様に 製作物という証拠と一緒にアピールをすると信憑性が高く評価されやすい みたいです。
もちろん今までの成績も評価されますが、成績のみで全てを決める場合はむしろ少ない様です(?)。
やはり、自分がやってきたことのアウトプットは大事だなと思いました。

今回、自分の過去のリポジトリで公開できる物を探したところ、とりあえずできそうだったのは2つでしたが、これを機にこれからは製作物をよりPublicに発信して行こうと思います。
私も研究室配属選考で勝ち残れる様に頑張ります!!!

客観的評価がついているとより強いと思うので、製作物が良いと思ったらリポジトリにスターください(小声)(願望)(切実)

おまけ

GitHubのプロフィールも作成し、それっぽくしてみました!

https://github.com/kntkymt

スクリーンショット 2020-10-17 12.21.39.png

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

Xcode12でplaygroundを作成する

Xcode12は、Welcome to Xcode画面から「Get started with a playground」がなくなり、かわりに「Open a project or file」が追加になりました。

「Create a new Xcode project」を選択しても、playgroundの項目はありません。

既存のプロジェクトを開いた状態で下記画像の操作を行い、playgroundを作成することができました。

スクリーンショット 2020-10-21 11.16.49.png

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

【 1jpeg1knowledge 】XCode と xcworkspace は違うのよぉ。??

#1j1k XCWorkSpace vs XCodeProject.jpg

1peg1knowledge というのは、インスタの流行りのせいで、もはや文字情報の伝達がダルくなった平成世代を中心に画像で知識を共有する試みです。

ちなみに自分はインスタで女の人のパンケーキとかタピオカとか加工まみれの顔みたいな甘ったるい画像を見まくったあとにYouTubeで長渕剛のライブを見るのがマイブームです。
サウナのあとの水風呂効果です。

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

【 1jpeg1knowledge 】xcodeproj と xcworkspace は違うのよぉ。??

#1j1k XCWorkSpace vs XCodeProject.jpg

【1peg1knowledge】 というのは、インスタの流行りのせいで、もはや文字情報の伝達がダルくなった平成世代を中心に画像で知識を共有する試みです。
ちなみに自分はインスタで女の人のパンケーキとかタピオカとか加工まみれの顔みたいな甘ったるい画像を見まくったあとにYouTubeで長渕剛のライブを見るのがマイブームです。
サウナのあとの水風呂効果です。

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

[Swift5]"IBM Watson ToneAnalyzer"で取得した分析結果をJSONに変換する

投稿のポイント

今回は前回投稿した記事の続きなので、前回記事をまだみられていない方はまず、そちらをご覧ください。

[Swift5]"IBM Watson ToneAnalyzer"を使用して感情分析を行う
https://qiita.com/nkekisasa222/items/2933e46b22c17d3eedfb

今回行うことと、記述したコード

前回取得した下記情報をJSON形式に変換します。

ToneAnalysis(documentTone: ToneAnalyzer.DocumentAnalysis(tones: Optional([ToneAnalyzer.ToneScore(score: 0.6165, toneID: "sadness", toneName: "Sadness"), ToneAnalyzer.ToneScore(score: 0.829888, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, warning: nil), sentencesTone: Optional([ToneAnalyzer.SentenceAnalysis(sentenceID: 0, text: "Team, I know that times are tough!", tones: Optional([ToneAnalyzer.ToneScore(score: 0.801827, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil), ToneAnalyzer.SentenceAnalysis(sentenceID: 1, text: "Product sales have been disappointing for the past three quarters.", tones: Optional([ToneAnalyzer.ToneScore(score: 0.771241, toneID: "sadness", toneName: "Sadness"), ToneAnalyzer.ToneScore(score: 0.687768, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil), ToneAnalyzer.SentenceAnalysis(sentenceID: 2, text: "We have a competitive product, but we need to do a better job of selling it!", tones: Optional([ToneAnalyzer.ToneScore(score: 0.506763, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil)]))

今回記述したのは以下のコード。

ViewController.swift
//ステータスコードの定数を作成し条件分岐
let statusCode = response?.statusCode
  switch statusCode == Optional(200)  {
    case true:
      print("分析成功: \(statusCode)")
      //分析結果の定数を作成
      let analysisResult = result

      //JSONへ変換するencoderを用意
      let encoder = JSONEncoder()

      //可読性を高めるためにJSONを整形
      encoder.outputFormatting = .prettyPrinted

      //分析結果をJSONに変換
      guard let jsonValue = try? encoder.encode(analysisResult) else {
        fatalError("Failed to encode to JSON.")
      }

      //JSONデータ確認
      print("感情分析結果(JSON): \(String(bytes: jsonValue, encoding: .utf8)!)")

  case false:
    //ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー)
    print("分析失敗: \(statusCode)")
}

コード解説

まず、ToneAnalyzerに送ったリクエストに対して帰ってくる値resposestatusCodeのインスタンスを作成し、コード(200)を条件としてswitch文で分岐します。ステータスコードの概要は以下の画像を参考にしてください。
image.png

続いてこちらのコードを解説。

ViewController.swift
//分析結果を代入
let analysisResult = result

//JSONへ変換するencoderを用意
let encoder = JSONEncoder()

//可読性を高めるためにJSONを整形
encoder.outputFormatting = .prettyPrinted

//分析結果をJSONに変換
guard let jsonValue = try? encoder.encode(analysisResult) else {
  fatalError("Failed to encode to JSON.")

//JSONデータ確認
print("感情分析結果(JSON): \(String(bytes: jsonValue, encoding: .utf8)!)")
}

まず、分析結果をanalysisResultに代入し、次にJSONEncoder()をencoderに代入。このJSONEncoder()を使ってJSON形式に変換します。

続いて、JSONを人間の視覚的に感知しやすい形式にフォーマットを整形。そして、encoder.encode(#ここに分析結果)メソッドを用いて変換。

最後に、printを記述してビルド。

分析成功: Optional(200)
感情分析結果(JSON): {
  "sentences_tone" : [
    {
      "sentence_id" : 0,
      "text" : "Team, I know that times are tough!",
      "tones" : [
        {
          "score" : 0.80182699999999996,
          "tone_id" : "analytical",
          "tone_name" : "Analytical"
        }
      ]
    },
    {
      "sentence_id" : 1,
      "text" : "Product sales have been disappointing for the past three quarters.",
      "tones" : [
        {
          "score" : 0.77124099999999995,
          "tone_id" : "sadness",
          "tone_name" : "Sadness"
        },
        {
          "score" : 0.68776800000000005,
          "tone_id" : "analytical",
          "tone_name" : "Analytical"
        }
      ]
    },
    {
      "sentence_id" : 2,
      "text" : "We have a competitive product, but we need to do a better job of selling it!",
      "tones" : [
        {
          "score" : 0.50676299999999996,
          "tone_id" : "analytical",
          "tone_name" : "Analytical"
        }
      ]
    }
  ],
  "document_tone" : {
    "tones" : [
      {
        "score" : 0.61650000000000005,
        "tone_id" : "sadness",
        "tone_name" : "Sadness"
      },
      {
        "score" : 0.82988799999999996,
        "tone_id" : "analytical",
        "tone_name" : "Analytical"
      }
    ]
  }
}

デバックエリアにこのように表示されていれば変換成功です!

一応、前回コードと合わせたものを記述しておきます。

ViewController.swift
//ToneAnalyzer(感情分析)用メソッド
  func toneAnalyzer() {

    //WatsonAPIキーのインスタンス作成
    let authenticator = WatsonIAMAuthenticator(apiKey: "")

    //WatsonAPIのversionとURLを定義
    let toneAnalyzer = ToneAnalyzer(version: "2017-09-21", authenticator: authenticator)
        toneAnalyzer.serviceURL = ""

    //分析用サンプルテキスト
    let sampleText = """
    Team, I know that times are tough! Product \
    sales have been disappointing for the past three \
    quarters. We have a competitive product, but we \
    need to do a better job of selling it!
    """

    //SSL検証を無効化(不要?)
    //toneAnalyzer.disableSSLVerification()

    //エラー処理
    toneAnalyzer.tone(toneContent: .text(sampleText)){
      response, error in
        if let error = error {
          switch error {
            case let .http(statusCode, message, metadata):
              switch statusCode {
              case .some(404):
                // Handle Not Found (404) exceptz1zion
                print("Not found")
              case .some(413):
                // Handle Request Too Large (413) exception
                print("Payload too large")
              default:
                if let statusCode = statusCode {
                  print("Error - code: \(statusCode), \(message ?? "")")
                }
              }
            default:
              print(error.localizedDescription)
            }
            return
          }
          //データ処理
          guard let result = response?.result else {
            print(error?.localizedDescription ?? "unknown error")
            return
          }
          //ステータスコードの定数を作成し条件分岐
          let statusCode = response?.statusCode
            switch statusCode == Optional(200)  {
                case true:
                    print("分析成功: \(statusCode)")
                    //分析結果の定数を作成
                    let analysisResult = result

                    //JSONへ変換するencoderを用意
                    let encoder = JSONEncoder()

                    //可読性を高めるためにJSONを整形
                    encoder.outputFormatting = .prettyPrinted

                    //分析結果をJSONに変換
                    guard let jsonValue = try? encoder.encode(analysisResult) else {
                        fatalError("Failed to encode to JSON.")
                    }

                    //JSONデータ確認
                    print("感情分析結果(JSON): \(String(bytes: jsonValue, encoding: .utf8)!)")
                    //ヘッダーパラメータ
                    print(response?.headers as Any)

                case false:
                    //ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー)
                    print("分析失敗: \(statusCode)")
            }
        }
    }

最後に

この後は取得したJSONから値を取得していこうと考えております。もし、コード内容に修正の余地があればぜひご教授頂ければ幸いです。

最後までご覧いただきありがとうございました!

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

俺的RxSwiftまとめ②

RxSwiftの特徴をさらに詳しく

この記事は、俺的RxSwiftまとめ①の続きです。

RxSwiftの特徴

非同期処理には、気を付けるポイントが2点ある。

  • コードの実行順序
  • 共有されたmutableなデータをどのように取り扱うか

である。

RxSwiftは、これらの問題に対して、以下の2つの概念を取り入れて、対処している。

そして、以下の5つの特徴を手に入れている。(=Reactive System)

  • Resposive→UIにアプリの最新状態を常に反映すること
  • Resilient→各処理が分離されていて、エラーリカバリが容易であること
  • Elastic→変動ワークロードに対して、遅延読み込みスロットリング、リソースシェアなどの機能で対処する
  • Message-driven→コンポーネント間のやりとりをメッセージベースの通信を使用して非同期に行い、疎結合にして再利用性を高め、クラスのライフサイクルと別に実装すること

RxSwiftの構成要素

RxSwiftはObservable/operator/schedulerの3つの構成要素を持っている。

Observable

Observable<type>で定義する、観察対象のこと。
時間の経過とともに生成される一連のデータの不変なsnapshot(その瞬間のデータのコピー)を流す一連のイベントを、非同期に生成することができる。

複数のobserver(観察者)がリアルタイムにイベントに反応して、UIを更新したり、データを利用できる。
ObservableType protocolは以下の3つのイベントを生成することができる。

  • next → 次のデータをobserverに持ってくるイベント。completedが起こるまで、値をobserverに持ってき続ける。
  • completed → 一連のイベントをsuccessで終了させ、observerに通知する。
  • errorobservableerrorで終了したことを通知する。

Operators

Observable<type>には、非同期、イベントに基づいた処理を行うメソッド(Operators)が多数含まれている。これらはSide effect(ユーザー側にUIで反映すること)なしに出力のみを生成するので、Operatorを組み合わせて入力を任意の値に変換することができる。
代表的なOperatorを挙げると、

  • filiter → 条件に合う値のみを抽出する
  • mapObservableで流れてきた全ての値に対して処理を行う
  • skip → 特定の値をスキップする

などがあります。(後ほど別記事でまとめたいと思います。)

Scehduler

SchedulerDispatch queueと同じようなもので、処理をメインスレッドとサブスレッドで分けることができる。RxSwiftは定義済みのSchedulerがたくさんあるので、便利!
*UIの更新はメインスレッドで行う

RxCocoa

RxSwiftは、Swiftに限らないRxの共通使用に関するものだ。Swift特有のUIKitなどは、RxCocoaを用いて扱っていく。
RxCocoaは多くのUIパーツにリアクティブな機能を追加しているライブラリである。

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

【Swift】複数行のUILabelの余白を設定する:上下左右

はじめに

環境は
・Xcode 11.6
・Swift 5
になります。

ラベルの余白(パディング)を設定したいとき、ありますよね?

例えばtextViewの場合はtextContainerInset、UIButtonの場合はcontentEdgeInsetsなんかを使って、比較的楽に設定できるかと思います。

しかしUILabelの場合は少し面倒で、複数行のテキストだと尚更。

まず試すこと

特に横方向の余白については、attributedTextできれいに解決するかも。
以下は一例。

let style = NSMutableParagraphStyle()
// horizontal setting
style.headIndent = 0
style.tailIndent = 0
// vertical setting
style.lineSpacing = 0
style.maximumLineHeight = 16
style.minimumLineHeight = 16
style.paragraphSpacingBefore = 10
style.paragraphSpacing = 30

let attr: [NSAttributedString.Key : Any] = [
    .font: ...,
    .paragraphStyle : style,
]
let attributedText = NSAttributedString(string: "hoge", attributes: attr)

let label = UILabel()
label.attributedText = attributedText

縦方向については、行間隔と行の高さを指定できるものの、直接余白を設定できないのが悔しい。
ただしテキストが1行の場合は、行の高さとフォントサイズをうまく設定して、実質的に上下の余白をコントロールするやり方も。

大抵はこれでオッケー

検索すればよく出てくるやり方だが、以下のようにUILabelを継承したカスタムLabelを作ってやれば、大抵は解決する。

きちんと上下左右の余白を設定できる。

class PaddingLabel: UILabel {

    var padding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)

    override func drawText(in rect: CGRect) {
        let newRect = rect.inset(by: padding)
        super.drawText(in: newRect)
    }

    override var intrinsicContentSize: CGSize {
        var contentSize = super.intrinsicContentSize
        contentSize.height += padding.top + padding.bottom
        contentSize.width += padding.left + padding.right
        return contentSize
    }

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        var contentSize = super.sizeThatFits(size)
        contentSize.width += padding.left + padding.right
        contentSize.height += padding.top + padding.bottom
        return contentSize
    }
}

ここでsizeThatFits(_ size: CGSize)は無くても良いが、これがあるとsizeToFit()したときに余白を含んだ大きさで自動調整してくれるので、ありがたい。
もちろん、sizeToFit()で余白を含めたくなければ、オーバーライドしないでおく。

最終手段

以上で解決しない問題があったら、おそらく最終手段は

UILabel( ) in UIView( )

として、ラベルは通常のsizeToFit()で余白なしの状態にしてから親ビュー内の任意の位置に配置することによって、親ビューを余白つきのラベル(もしくはボタン)として扱える。

この場合は親ビューのサイズをこちらで指定しないといけないが、sizeToFit()+AutoLayoutで半自動化はできるはず。

最後に

他に良い方法があったら教えてください!

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

【Swift】生成した動画のサムネイル画像の画質が悪いと思ったら

悩んだこと

カメラロールを自力で実装すると、動画の場合はプレビュー画像を自分で作ってあげることになると思います。

作成したプレビュー画像を確かめてみたら、どうも画質が悪くてぼやけている...
この原因を探すのに少し時間がかかってしまいました。

解決方法

以下のソース中でgenerator.maximumSizeに適切な値を設定してあげることで解決しました!

let asset: AVAsset = ...
let generator = AVAssetImageGenerator(asset: asset)
generator.maximumSize = CGSize(width: w, height: h)
generator.appliesPreferredTrackTransform = true
let img = try! generator.copyCGImage(at: CMTime.zero, actualTime: nil)

わかってしまえば何のことはないが、意外とハマった。

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

【Xcode】まずはこれだけ、便利なショートカット

マストなアイテム(キーボードショートカット)を集めてみました。
環境はXcode 11.6です。

複雑なショートカットはよく忘れてしまいますが(カスタマイズしろよ!)、これらはよく使うのですぐに慣れるはず。

検索

cmd + F

現在開いているファイル内で検索。

cmd + shift + F

全てのファイルから検索。

cmd + shift + O

ファイル名や関数名を検索。
だいぶ便利!

ビルドと実行

以下の2つはセットで覚える。

cmd + R

実行。

cmd + .

実行(ビルド)を中止。

表示

cmd + shift + J

「いま編集しているファイル、どのディレクトリ階層にあるんだっけ?」

cmd + 0

左メニューの表示/非表示を切り替える。

cmd + 1~9

左メニューの中の表示を切り替える。

編集

右クリック、 cmd + クリック

シンボルを右クリックしたり、cmdを押しながらクリックすると、各種アクションが表示される。
右クリック → Refactor → Renameはよく使う。

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

お天気APIを用いて傘が必要か判断するアプリ作成 備忘録

はじめに

初心者が学習とポートフォリオをかねて作成しました。
作成し始めて1〜2ヶ月程度ですので、リファクタリングやコーディング等のアドバイスあればいただきたく。

アプリ概要

サラリーマンなら今日傘が必要かを一瞬で確認できるアプリがあれば便利だなと思い作成。

ボタンを押す

GPSから今いる場所の緯度・経度を割り出す

緯度・経度から逆ジオコーディング

今いる都道府県を割り出す

お天気APIから今いる都道府県の降水確率を割り出す

傘が必要かそうでないかを表示

すでにそのようなアプリがリリースされていましたが、僕が望んでいたアプリとは違いましたので、ポートフォリオ作成もかねて作成してみました。

動作環境

対象 バージョン
iOS 14.0
macOS Catalina 10.15.7
Xcode 12.0
Swift 5.3

使用したお天気API
OpenWeatherMap

Podをインストールする

今回は
"Alaomofire”と”SwiftyJSON”をインストールしました。

Podfile
pod "Alamofire"
pod "SwiftyJSON"

参考サイト

Main.storyboadを作成する

Main.storyboad
スクリーンショット 2020-10-20 21.01.42.png

今回はHomeVCとUmbrellaVCを作成しました。
アプリ起動後にHomeVCへ移行→「Location Information」ボタンタップ→UmbrellaVCへ移行
①labelには、逆ジオコーディングで取得した現在地の都道府県を呼び出し
②labelには、お天気APIから現在地の降水確率を呼び出し
③labelには、降水確率から傘が必要か不必要か判断

実装する

まずはお天気APIで取得するデータのmodelを作成しておく。

CityModel
CityModel.swift
import Foundation

struct cityModel:Decodable{

    var list: [List]

    struct List:Decodable {
        var pop:Double
    }

}

ここに記載する構造体は取得するAPIデータによって異なると思います。
今回使用したお天気APIの「OpenWeather」では、上記のような取得をしました。

OpenWeatherで取得したJSONデータ

この中の”pop”という項目の数値が今回取得しようとしている数値です(Double型)
スクリーンショット 2020-10-20 21.14.22.png

参考サイト


UmbrellaVC
UmbrellaVC.swift
import UIKit
import CoreLocation

class UmbrellaVC: UIViewController {

    @IBOutlet weak var label1: UILabel!
    var locationText:String = "大阪府"

    @IBOutlet weak var label2: UILabel!
    var popText:Int = 0

    @IBOutlet weak var label3: UILabel!
    var umbrellaJudgement:String = "傘が必要"

    override func viewDidLoad() {
        super.viewDidLoad()
        label1.text = locationText
        label2.text = "\(popText)" + "%"
        label3.text = umbrellaJudgement

        self.label3.layer.borderWidth = 2.0
        self.label3.layer.borderColor = UIColor.black.cgColor
        self.label3.layer.cornerRadius = 20

        //降水確率によって出てくる文字の色を変えて一目でわかるようにする
        if popText >= 30{
            label3.textColor = .orange

        }else if popText >= 70{
            label3.textColor = .red
        }
    }
}


各labelに変数を代入しておく。


UmbrellaVCで設定した変数に代入する。

HomeVC
HomeVC.swift
import UIKit
import CoreLocation
import Alamofire
import SwiftyJSON

class HomeVC: UIViewController,CLLocationManagerDelegate {

    //各変数へ初期値を入れておく
    var latitudeNow: Double = 39.0000
    var longitudeNow: Double = 140.0000
    var locationManager: CLLocationManager!
    var administrativeArea:String = ""
    var locationNow: String = ""
    private var citymodel: cityModel?
    var doubleOfMaximumPop:Double = 100.0
    var maxPop:Int = 30
    var Judge:String = ""

 override func viewDidLoad() {
        super.viewDidLoad()

        //image viewのレイアウト設定
        umbrellaImage.image = UIImage(named:"umbrellaImage")
        umbrellaImage.layer.cornerRadius = 10

        //locationManagerをviewDidload時に呼び出す(位置情報を更新する)
        locationManagerDidChangeAuthorization(CLLocationManager())

        //天気取得関数の呼び出し
        getWeatherData()

    }

    //    ボタンを押した際に位置情報を取得する
    @IBAction func buttonTapped(_ sender: Any) {

        //ボタンを押すとlocationManagerの更新をやめる
        stopLocationManager()

   }

    //    ボタンを押すとsegueに移行し、UmbrellaVC内のLabel1に都道府県を記載する
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if(segue.identifier == "toUmbrellaVC") {
            let  locationNow: UmbrellaVC = (segue.destination as? UmbrellaVC)!
            locationNow.locationText = administrativeArea

            let popNow:UmbrellaVC = (segue.destination as? UmbrellaVC)!
            popNow.popText = Int(maxPop)

            let umbJud:UmbrellaVC = (segue.destination as? UmbrellaVC)!
            umbJud.umbrellaJudgement = Judge

        }
    }

    //ロケーションマネージャー@iOS 14 位置情報の更新
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        locationManager = CLLocationManager()

        let status = manager.authorizationStatus

        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            locationManager.delegate = self
            locationManager.startUpdatingLocation()

        case .notDetermined, .denied, .restricted:
            showAlert()

        default:print("未処理")
        }
    }

    //locationManagerの情報更新をやめる
        func stopLocationManager(){
            locationManager.stopUpdatingLocation()
        }

    //アラートを表示する関数
    func showAlert(){
        let alertTitle = "位置情報取得が許可されていません。"
        let alertMessage = "設定アプリの「プライバシー > 位置情報サービス」から変更してください。"
        let alert: UIAlertController = UIAlertController(
            title: alertTitle, message: alertMessage, preferredStyle: UIAlertController.Style.alert
        )
        //OKボタン
        let defaultAction: UIAlertAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)

        //UIAlertControllerにActionを追加
        alert.addAction(defaultAction)
        present(alert, animated: true, completion: nil)

    }

    //位置情報が更新された際、位置情報を格納する関数
    //位置情報が更新されないとlocation managerは起動しない※重要
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {


        let location = locations.first
        let latitude = location!.coordinate.latitude
        let longitude = location!.coordinate.longitude
        //位置情報を格納する
        self.latitudeNow = Double(latitude)
        self.longitudeNow = Double(longitude)

        //位置情報を取得後、逆ジオコーディングし、都道府県を割り出す
        let locationA = CLLocation(latitude: latitudeNow, longitude: longitudeNow)

        let geocoder: CLGeocoder = CLGeocoder()
        geocoder.reverseGeocodeLocation(locationA) { [self] (placemarks, error) in
            if let placemark = placemarks?.first {
                self.administrativeArea = placemark.administrativeArea!

            } else {
                self.administrativeArea = "bb"
            }
        }
    }

    //天気予報APIを用いて18時間後までの最大降水確率を取得する
    //OpenWeatherMapでは1日の降水確率取得が有料であり、3時間ごとの降水確率取得は無料のためそちらを使用(朝出勤前にみることを想定したため、問題ないと判断)
    private func getWeatherData()  {

        let id = "API ID を入力"
        let baseUrl = "http://api.openweathermap.org/data/2.5/forecast?lat=" + "\(latitudeNow)" + "&lon=" + "\(longitudeNow)" + "&exclude=daily&lang=ja&cnt=6&.pop&appid=" + "\(id)"

        AF.request(baseUrl, method: .get).responseJSON { [self] response in
            guard let data = response.data else {
                return
            }
            do {
                let citymodel = try JSONDecoder().decode(cityModel.self, from: data)

                //APIのデータをリスト表示する
                let popNumber = citymodel.list.map{ $0.pop }

                //リスト内のmaxデータを取得する
                var doubleOfMaximumPop = popNumber.max()

                //maxデータのパーセンテージ表示に変換する
                let maxPop = doubleOfMaximumPop! * 100

                //データがあるかどうかを判断する
                if doubleOfMaximumPop == nil{
                    print(Error.self)
                }else {
                    //データがあれば、
                    if doubleOfMaximumPop != nil{
                        //maxデータを取得する
                        doubleOfMaximumPop = self.doubleOfMaximumPop
                    }else {
                        //同じ数字であれば、その中のひとつをピックアップする
                        doubleOfMaximumPop = popNumber[0]
                    }
                }

                //maxPopへgetweather関数で取得した数値を変数へ代入する
                self.maxPop = Int(maxPop)

                //maxPopによって傘が必要かの判断をし、判断した文をJudgeへ代入する。
                if self.maxPop <= 30 {
                    self.Judge = "⛅️傘は不要です⛅️"
                }else if self.maxPop >= 70 {
                    self.Judge = "☔️傘が必要です☔️"
                }else {
                    self.Judge = "☂️折り畳み傘を持っていれば安心☂️"
                }

            }catch let error {
                print("Error:\(error)")
            }
        }
    }
}

変数名が何を表しているかがほぼわからないため、可読性がない・・・。
変数名難しい・・・。

苦労した点

・LocationManagerで位置情報を更新→逆ジオコーディングしデータを取得するという流れを理解していなかったため、データは取得できているが、変数に代入されないなどの不具合が発生した。

参考サイト一覧

iOS14でのCore Location変更点
関数内の変数を関数外の変数へ代入する
Codaleについて備忘録

インプット

読書

・絶対に挫折しないiPhoneアプリ開発「超」入門編
・たった2日でマスターできるiPhoneアプリ開発集中講座 (どう考えても2日でマスターできる訳がない)

Udemy

・iOS12:Learn to Code & Build Real iOS 12 Apps in Swift 4.2
ゴリゴリの英語で結構キツかったが、概念などをイメージ図などで教えてくれたので、理解しやすかった。
iOSは随時更新されていくので、新しい知識を常にキャッチアップしなければならないとおもった。

アウトプット

Qiita

多くの方がQiitaにソースコードを載せてくれてたおかげでここまで作成することができたため、
同じような境遇の人たちの役に立てばと思い作成した。

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