- 投稿日:2020-10-21T21:48:12+09:00
SwiftUI 2.0 でカスタムタブビューを作ってみる
SwiftUI2.0で追加されたAPI
SwiftUI 2.0 で追加された
PageTabViewStyle
を使ってみたかったので、カスタムタブのようなものを作ってみました。とりあえずの完成形
こういったものを作っていこうと思います。
GitHubはこちらです。
https://github.com/hoshi005/custom-tab開発環境
- Xcode 12.1
- iOS 14.1
事前準備
- アニメーションgifを利用したかったのでSDWebImageSwiftUIを利用しています。
- ぴよたそさんからアニメーションgifファイルを利用させてもらっています。
タブ部分の作成
アニメーション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) } }出来上がったのはこちらです。このままだと、選択状態がよくわからないですね。
選択状態がわかるように見た目を調整する
選択時/非選択時で見た目を切り替えるため、
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に. } } }見た目はこのようになります。選択状態が一目でわかるようになりました。
背景色の設定と、タブの配置調整
ContentView
の見た目を調整します。
- 全体を
ZStack
で囲う- 最背面に
Color("bg").ignoresSafeArea()
を配置して背景色とする- タブビュー部分を
VStack
とSpacer
を利用して画面下部に配置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) } } }画面をタブで切り替える
タブは用意したので、このタブに連動して画面が切り替わるようにします。
まずはダミーで画面部分を用意します。
適当なので、こちらは好きに作ってもらって良いと思います。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」を簡単に作成できるのは嬉しいですね。
こちらの記事で作った切替ビューでも同じようなことができそうです。もしよかったら試してみてください。
- 投稿日:2020-10-21T21:26:41+09:00
【iOS】FloatingPanelを使ってセミモーダルビューを表示する
今回の目的
これを表示してみたいって人向けの話です。
環境
Xcode 12.0.1
Swift5
CocoaPods 1.10.0環境設定
podファイルに下記を追加してinatall
pod 'FloatingPanel'本題
では早速作っていこう
Storyboard
今回のゴールは先ほどのgifの通りボタンをタップするとセミモーダルビューが出てくるところ。
まずは2画面用意し、片方はボタンを設置する。(segueは不要)ViewController : ボタンを配置した方
SemiModalViewController :表示するモーダル。見やすいようにオレンジにした
ViewController
ViewController.swiftimport 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.swiftimport 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] } }まとめ
見事セミモーダルビューを表示することに成功
次はセミモーダルビュー に値渡しする方法を書く参考
- 投稿日:2020-10-21T17:56:16+09:00
Zendesk Chat SDK を使ってiOSアプリにカスタマーサポートチャットを1時間で実装する
はじめに
はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
(Chat SDK v2 for iOSを利用しています)zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。zendesk chatの説明
このようなチャットの画面を簡単に構築できるツールです。
詳細な料金表はこちら
無料含めて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に渡すaccountKey
とappId
って
どこに書いてあるんだ!ということです。
accountKey
はこちら、chat > 右上のアイコン > 接続を確認
で確認できます。
appId
はメソッドの定義を見るとappId: String? = nil
と、
appIdを指定し無くても実行できます。無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。
appId
は後半の Push通知についての箇所で明記するので
いったんChat.initialize(accountKey: チャットアカウントキー)
だけで動かして
先に進んでしまっても問題ありません。ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。// 匿名ユーザーとして作成 let anonymous = Identity.createAnonymous() Zendesk.instance?.setIdentity(anonymous)これらを
AppDelegate.swift
のdidFinishLaunchingWithOptions
で初期化します。AppDelegate.swiftfunc 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のページから作業します。
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 OKpemが正しく生成されたのかテストを実行
> 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が生成されます。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.swiftimport ChatProvidersSDK func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Chat.registerPushToken(deviceToken) }・ push通知をタップしたときの挙動
AppDelegate.swiftimport 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.swiftimport UserNotifications func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound, .badge]) }まとめ
- チャット画面を開いて、エージェントとチャットができた
- エージェントのオンライン状態をアプリ側でフックできた
- チャットが来たときに Push通知を受け取ることができた
- 投稿日:2020-10-21T17:56:16+09:00
zendesk chat ios SDK でチャットができてPush通知が飛ぶまでの手順
はじめに
はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
(Chat SDK v2 for iOSを利用しています)zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。zendesk chatの説明
このようなチャットの画面を簡単に構築できるツールです。
詳細な料金表はこちら
無料含めて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に渡すaccountKey
とappId
って
どこに書いてあるんだ!ということです。
accountKey
はこちら、chat > 右上のアイコン > 接続を確認
で確認できます。
appId
はメソッドの定義を見るとappId: String? = nil
と、
appIdを指定し無くても実行できます。無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。
appId
は後半の Push通知についての箇所で明記するので
いったんChat.initialize(accountKey: チャットアカウントキー)
だけで動かして
先に進んでしまっても問題ありません。ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。// 匿名ユーザーとして作成 let anonymous = Identity.createAnonymous() Zendesk.instance?.setIdentity(anonymous)これらを
AppDelegate.swift
のdidFinishLaunchingWithOptions
で初期化します。AppDelegate.swiftfunc 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のページから作業します。
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 OKpemが正しく生成されたのかテストを実行
> 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が生成されます。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.swiftimport ChatProvidersSDK func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Chat.registerPushToken(deviceToken) }・ push通知をタップしたときの挙動
AppDelegate.swiftimport 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.swiftimport UserNotifications func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound, .badge]) }まとめ
- チャット画面を開いて、エージェントとチャットができた
- エージェントのオンライン状態をアプリ側でフックできた
- チャットが来たときに Push通知を受け取ることができた
- 投稿日:2020-10-21T17:56:16+09:00
Zendesk Chat iOS SDK でチャットができてPush通知が飛ぶまでの手順
はじめに
はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
(Chat SDK v2 for iOSを利用しています)zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。zendesk chatの説明
このようなチャットの画面を簡単に構築できるツールです。
詳細な料金表はこちら
無料含めて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に渡すaccountKey
とappId
って
どこに書いてあるんだ!ということです。
accountKey
はこちら、chat > 右上のアイコン > 接続を確認
で確認できます。
appId
はメソッドの定義を見るとappId: String? = nil
と、
appIdを指定し無くても実行できます。無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。
appId
は後半の Push通知についての箇所で明記するので
いったんChat.initialize(accountKey: チャットアカウントキー)
だけで動かして
先に進んでしまっても問題ありません。ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。// 匿名ユーザーとして作成 let anonymous = Identity.createAnonymous() Zendesk.instance?.setIdentity(anonymous)これらを
AppDelegate.swift
のdidFinishLaunchingWithOptions
で初期化します。AppDelegate.swiftfunc 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のページから作業します。
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 OKpemが正しく生成されたのかテストを実行
> 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が生成されます。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.swiftimport ChatProvidersSDK func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Chat.registerPushToken(deviceToken) }・ push通知をタップしたときの挙動
AppDelegate.swiftimport 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.swiftimport UserNotifications func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound, .badge]) }まとめ
- チャット画面を開いて、エージェントとチャットができた
- エージェントのオンライン状態をアプリ側でフックできた
- チャットが来たときに Push通知を受け取ることができた
- 投稿日:2020-10-21T17:56:16+09:00
Zendesk Chat iOS SDK を使ってカスタマーサポートチャットを1時間で実装する
はじめに
はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
(Chat SDK v2 for iOSを利用しています)zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。zendesk chatの説明
このようなチャットの画面を簡単に構築できるツールです。
詳細な料金表はこちら
無料含めて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に渡すaccountKey
とappId
って
どこに書いてあるんだ!ということです。
accountKey
はこちら、chat > 右上のアイコン > 接続を確認
で確認できます。
appId
はメソッドの定義を見るとappId: String? = nil
と、
appIdを指定し無くても実行できます。無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、Push通知が届きません。
appId
は後半の Push通知についての箇所で明記するので
いったんChat.initialize(accountKey: チャットアカウントキー)
だけで動かして
先に進んでしまっても問題ありません。ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。// 匿名ユーザーとして作成 let anonymous = Identity.createAnonymous() Zendesk.instance?.setIdentity(anonymous)これらを
AppDelegate.swift
のdidFinishLaunchingWithOptions
で初期化します。AppDelegate.swiftfunc 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のページから作業します。
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 OKpemが正しく生成されたのかテストを実行
> 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が生成されます。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.swiftimport ChatProvidersSDK func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Chat.registerPushToken(deviceToken) }・ push通知をタップしたときの挙動
AppDelegate.swiftimport 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.swiftimport UserNotifications func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound, .badge]) }まとめ
- チャット画面を開いて、エージェントとチャットができた
- エージェントのオンライン状態をアプリ側でフックできた
- チャットが来たときに Push通知を受け取ることができた
- 投稿日:2020-10-21T14:22:06+09:00
ユニットテストを実践して感じた、初心者が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.swiftpublic 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.swiftpublic 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.swiftfinal 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]値型:
ミュータブルな型とイミュータブルな型の相違を知ろう
純粋値型Swiftsubscript:
Swift の Subscript についてprotocol指向:
WWDC 2015 Swiftで値型でより良いアプリを作るenum:
Swiftの列挙型(enum)おさらい
[Swift] enumはprotocolに準拠できるので、例えばComparableによってシンプルに比較できる
- 投稿日:2020-10-21T13:26:12+09:00
NavigationControllerのBackをコードから
一つ前のViewControllerにもどる
self.navigationController?.popViewController(animated: true)?
お仕事のご相談こちらまで
rockyshikoku@gmail.comCore MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-10-21T12:32:49+09:00
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.plist2.デフォルトのテンプレートは以下なので、これをコピペする。
<?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の場合、以下の設定を編集する。
・ファイルテンプレートについての説明
https://pleiades.io/help/objc/using-file-and-code-templates.html・変数の一覧
https://pleiades.io/help/objc/file-template-variables.htmlCopyright~~の文は組織名($ORGANIZATION_NAME)が定義されていないと表示されない。
設定するには「⌘;」で設定を開き、GeneralタブからOrganizationを編集する。
- 投稿日:2020-10-21T12:07:17+09:00
大学生が研究室配属選考での自己アピールのためにGitHubで製作物を公開した話
経緯
大学の情報系学科に通っている大学3年生です!
私の通っている大学・学科では、3年生の後半(10~11月)に研究室配属があります。この研究室配属では、各研究室が成績+面接等によって希望者の中から配属者を選考するのですが、面接では研究への興味・プログラミング能力・継続力・学習意欲などの自己アピールを求められます。そして、ここで重要なのは自己アピールにはGitHub等にあげた製作物も利用することができるという点です。
今回、私も研究室配属選考に備え、自己アピールに使うために過去の製作物をGitHubで公開し、「せっかく公開するんだったら記事でも書いてみようかな」と思って記事を書いて見ました。
製作物
Qiita_for_iOS
iOS開発の学習のために作成した
俺得Qiita閲覧アプリです。
- 記事の閲覧・検索やLGTM・ストック、ストック記事の確認などができます。
- Qiitaの公開APIを使用
- iOS13のキャッチアップを兼ねて作成したので、
UICollectionViewCompositionalLayout
やProperty Wrapper
などを盛り込んでいます。discord_clone_firebase
React開発の学習のために作成したチャットサービスDiscordのクローンアプリです。
デモ: https://discord-clone-36c89.web.app/
※ ユーザー名の入力が求められますが、「test」等を入力していただければ大丈夫です。
- メッセージ送信, 画像・ファイル送信, AddReactionなどができます
- バックエンドにFirebaseを利用
- React + Typescript + React Hooks
大学2年生以下の開発者様へ
製作物があると自己アピールに使える武器が手に入るので、作っておいて損はないと思いますし、研究室配属選考に限らず、その自己アピールが使えるケースも多いのではないでしょうか。
私の場合は製作物がiOS, Reactアプリケーションなので研究内容に直結しにくいですが、それでもプログラミング能力・継続力・学習意欲などのアピールになると思いますし、更に近年は機械学習・画像処理・音声認識などの比較的研究に関連しやすい分野も個人開発で手を出せるので、製作物で研究への興味や経験をアピールすることも可能だと思います。
もし、大学2年生以下で開発をしているのであれば、1つで良いのである程度形になった・公開できる製作物を作っておくと研究室配属で役に立つかもしれません!!
まとめ
(大学の先輩から聞いた話なのですが)
研究室配属選考の面接だと、「〇〇に興味があります。」 「プログラミングは得意です。」等の口頭での自己アピールのみの学生も多いらしく、その中で「〇〇に興味があります。〇〇を作りました。ソースコードはGitHubに上げてあります。」 「プログラミングは得意です。〇〇を作りました。△△にリリースしてあります。」と言った様に 製作物という証拠と一緒にアピールをすると信憑性が高く評価されやすい みたいです。
もちろん今までの成績も評価されますが、成績のみで全てを決める場合はむしろ少ない様です(?)。
やはり、自分がやってきたことのアウトプットは大事だなと思いました。今回、自分の過去のリポジトリで公開できる物を探したところ、とりあえずできそうだったのは2つでしたが、これを機にこれからは製作物をよりPublicに発信して行こうと思います。
私も研究室配属選考で勝ち残れる様に頑張ります!!!客観的評価がついているとより強いと思うので、製作物が良いと思ったらリポジトリにスターください(小声)(願望)(切実)
おまけ
GitHubのプロフィールも作成し、それっぽくしてみました!
- 投稿日:2020-10-21T11:30:14+09:00
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-21T10:17:22+09:00
【 1jpeg1knowledge 】XCode と xcworkspace は違うのよぉ。??
- 投稿日:2020-10-21T10:17:22+09:00
【 1jpeg1knowledge 】xcodeproj と xcworkspace は違うのよぉ。??
【1peg1knowledge】 というのは、インスタの流行りのせいで、もはや文字情報の伝達がダルくなった平成世代を中心に画像で知識を共有する試みです。
ちなみに自分はインスタで女の人のパンケーキとかタピオカとか加工まみれの顔みたいな甘ったるい画像を見まくったあとにYouTubeで長渕剛のライブを見るのがマイブームです。
サウナのあとの水風呂効果です。
- 投稿日:2020-10-21T08:50:45+09:00
[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
に送ったリクエストに対して帰ってくる値respose
のstatusCode
のインスタンスを作成し、コード(200)
を条件としてswitch文
で分岐します。ステータスコードの概要は以下の画像を参考にしてください。
続いてこちらのコードを解説。
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(#ここに分析結果)
メソッドを用いて変換。最後に、
分析成功: 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から値を取得していこうと考えております。もし、コード内容に修正の余地があればぜひご教授頂ければ幸いです。
最後までご覧いただきありがとうございました!
- 投稿日:2020-10-21T08:02:31+09:00
俺的RxSwiftまとめ②
RxSwiftの特徴をさらに詳しく
この記事は、俺的RxSwiftまとめ①の続きです。
RxSwiftの特徴
非同期処理には、気を付けるポイントが2点ある。
- コードの実行順序
- 共有されたmutableなデータをどのように取り扱うか
である。
RxSwiftは、これらの問題に対して、以下の2つの概念を取り入れて、対処している。
- 宣言的プログラミング → 例:
a = b + c
と定義すれば、a
の値はb
とc
の値が変化するごとに更新する。- 関数型プログラミング → 関数の入出力の連なりで処理を記述する方式
そして、以下の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に通知する。error
→observable
がerror
で終了したことを通知する。Operators
Observable<type>
には、非同期、イベントに基づいた処理を行うメソッド(Operators
)が多数含まれている。これらはSide effect(ユーザー側にUIで反映すること)なしに出力のみを生成するので、Operator
を組み合わせて入力を任意の値に変換することができる。
代表的なOperator
を挙げると、
filiter
→ 条件に合う値のみを抽出するmap
→Observable
で流れてきた全ての値に対して処理を行うskip
→ 特定の値をスキップするなどがあります。(後ほど別記事でまとめたいと思います。)
Scehduler
Scheduler
はDispatch queue
と同じようなもので、処理をメインスレッドとサブスレッドで分けることができる。RxSwiftは定義済みのScheduler
がたくさんあるので、便利!
*UIの更新はメインスレッドで行うRxCocoa
RxSwift
は、Swift
に限らないRxの共通使用に関するものだ。Swift
特有のUIKit
などは、RxCocoa
を用いて扱っていく。
RxCocoa
は多くのUIパーツ
にリアクティブな機能を追加しているライブラリである。
- 投稿日:2020-10-21T02:14:44+09:00
【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
で半自動化はできるはず。最後に
他に良い方法があったら教えてください!
- 投稿日:2020-10-21T01:13:51+09:00
【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)わかってしまえば何のことはないが、意外とハマった。
- 投稿日:2020-10-21T00:56:49+09:00
【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はよく使う。
- 投稿日:2020-10-21T00:09:22+09:00
お天気APIを用いて傘が必要か判断するアプリ作成 備忘録
はじめに
初心者が学習とポートフォリオをかねて作成しました。
作成し始めて1〜2ヶ月程度ですので、リファクタリングやコーディング等のアドバイスあればいただきたく。アプリ概要
サラリーマンなら今日傘が必要かを一瞬で確認できるアプリがあれば便利だなと思い作成。
ボタンを押す
↓
GPSから今いる場所の緯度・経度を割り出す
↓
緯度・経度から逆ジオコーディング
↓
今いる都道府県を割り出す
↓
お天気APIから今いる都道府県の降水確率を割り出す
↓
傘が必要かそうでないかを表示すでにそのようなアプリがリリースされていましたが、僕が望んでいたアプリとは違いましたので、ポートフォリオ作成もかねて作成してみました。
動作環境
対象 バージョン iOS 14.0 macOS Catalina 10.15.7 Xcode 12.0 Swift 5.3 使用したお天気API
OpenWeatherMapPodをインストールする
今回は
"Alaomofire”と”SwiftyJSON”をインストールしました。Podfilepod "Alamofire" pod "SwiftyJSON"Main.storyboadを作成する
今回はHomeVCとUmbrellaVCを作成しました。
アプリ起動後にHomeVCへ移行→「Location Information」ボタンタップ→UmbrellaVCへ移行
①labelには、逆ジオコーディングで取得した現在地の都道府県を呼び出し
②labelには、お天気APIから現在地の降水確率を呼び出し
③labelには、降水確率から傘が必要か不必要か判断実装する
まずはお天気APIで取得するデータのmodelを作成しておく。
CityModel
CityModel.swiftimport Foundation struct cityModel:Decodable{ var list: [List] struct List:Decodable { var pop:Double } }ここに記載する構造体は取得するAPIデータによって異なると思います。
今回使用したお天気APIの「OpenWeather」では、上記のような取得をしました。
UmbrellaVC
UmbrellaVC.swiftimport 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.swiftimport 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で位置情報を更新→逆ジオコーディングしデータを取得するという流れを理解していなかったため、データは取得できているが、変数に代入されないなどの不具合が発生した。
参考サイト一覧