20210908のSwiftに関する記事は13件です。

ProgressViewをCustom ViewModifierで使いやすくする

はじめに SwiftUIには、ローディング中とかにぐるぐるするインジケーターがProgressViewとして用意されています。 大抵こういうViewは共通化されると思うので、使いやすくするためにProgressViewの Custom ViewModifierを作ってみました。 普通にViewにしてもいいと思ったのですが、 Viewにすると多分こうなってしまうと思います↓ 使う側では、毎回ZStackで囲わないといけないので、面倒な気がします。 ZStack { VStack { Text("Hello World") } if showProgressView { CustomProgressView() } } そうではなくこうやって使えた方が、ZStackとか不要だし便利だろうと思ったので、ViewModifierで実装してます。↓ VStack { Text("Hello World") .customProgressView(showProgressView: $showProgressView) } 成果物 よくあるローディング中画面が表示されています。 動画で見たい場合は、以下のGithubリポジトリで公開しているので、そのREADMEを確認ください。 ColorをProgressViewの前の階層に入れているので、この画面が表示されているときは後ろは操作不可にしてます。 ProgressViewだけでは、後ろは操作不可にしてくれませんので、自分で実装必要でした。 実装を見ればわかりますが、 「読み込み中」のテキストや色、背景色は自由にカスタマイズできます。 実装 まずCustom ViewModifierの実装です。 import SwiftUI struct CustomProgressView: ViewModifier { @Binding var showProgressView: Bool func body(content: Content) -> some View { // contentはこのカスタムViewModifierを使用する対象Viewのプロキシ ZStack { content if showProgressView { // ProgressViewの背景をタップ不可にするために、Colorを使用 Color.gray.opacity(0.2) VStack(spacing: 6) { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .gray)) .scaledToFit() .frame(width: 22, height: 22) Text("読み込み中") .foregroundColor(Color.gray) .font(.caption2) } } } } } 最初contentを書くのを忘れてしまい、ProgressViewは表示されるが背景にあるはずのViewが全く表示されないという現象で悩んでました。 contentは 修飾する対象のViewのプロキシ だそうなので、これがないと修飾する対象のViewが表示されないのは当然でした。。 なので、当然if文の外にcontentが存在してる必要があります。 そうでないと、showProgressViewがfalseの時には修飾する対象のViewも一緒に非表示になってしまいます。 ちなみにTextのViewを使わなくても、 ProgressView("読み込み")というようにテキストを設定できるのですが、 フォントサイズや色など細かくカスタマイズしたい場合は、TextのViewを自分で作らないといけないみたいです。 ProgressView(Text("読み込み").font(.caption2))というようには書けなかったです。 では次に、ViewのExtensionを実装します。 extension View { func customProgressView(showProgressView: Binding<Bool>) -> some View { self.modifier(CustomProgressView(showProgressView: showProgressView)) } } これで、初めに書いたように .customProgressView(showProgressView: $showProgressView) とModifierをつければ例の画面が表示できるようになります。 これがない場合は .modifier(CustomProgressView(showProgressView: $showProgressView)) と書くことになると思います。(実際にこれで動作確認はしていないのですが動くはず。。) これはこれでいいかもですが、私はラップした方が他の標準Modifierと同じように使えてシンプルかなと思っています。 今更な説明ですが、親View側でshowProgressViewの真偽を切り替えることになると思うので、@Bindingを使っています。 では早速使ってみる。 struct ContentView: View { @ObservedObject private var viewModel = ContentViewModel() var body: some View { VStack(spacing: 50) { Text("テスト1") Text("テスト2") } .background(Color.yellow) .frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height) .customProgressView(showProgressView: $viewModel.showProgressView) .onAppear { viewModel.onAppear() } } } viewModelクラスのプロパティとして、showProgressViewを@Publishedで持っています。 class ContentViewModel: ObservableObject { @Published var showProgressView = false func onAppear() { showProgressView = true // 3秒後待ってインジケータを削除(実際はAPI通信したり、画像取得したり) DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { self.showProgressView = false } } } 今回は3秒待つだけにしていますが、実際はAPIと通信したりとかとにかく何か時間がかかる処理が実装されることになると思います。 その処理完了の前後でshowProgressViewの真偽を切り替え、その値を監視することによって、 今回のローディング画面の切り替えを行なっています。 参考にした記事 SwiftUIのViewModifierを使ってViewをカスタマイズする 【SwiftUI】カスタムModifierの作成 How to create custom View modifiers for better code reusability in SwiftUI→かなり参考にさせていただきました。ProgressViewではなくもっとUI的に凝ったローディング画面を作りたい場合にはこの記事がおすすめです。 おわりに Custom ViewModifierを勉強する良い機会になりました。 まだまだSwiftUI初心者ですが、ちょっとずつ進歩してることを願ってます。 誤り、もっと良いやり方あるなどありましたら、是非コメントで教えてください 誰かの役に立てば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よくあるインジケーター表示切り替えを、ProgressViewとカスタム ViewModifierで使いやすくする

はじめに SwiftUIには、ローディング中とかにぐるぐるするインジケーターがProgressViewとして用意されています。 大抵こういうViewは共通化されると思うので、使いやすくするためにProgressViewの カスタム ViewModifierを作ってみました。 普通にViewにしてもいいと思ったのですが、 Viewにすると多分こうなってしまうと思います↓ 使う側では、毎回ZStackで囲わないといけないので、面倒な気がします。 ZStack { Text("Hello World") if showProgressView { CustomProgressView() } } そうではなくこうやって使えた方が、ZStackとか不要だし便利だろうと思ったので、ViewModifierで実装してます。↓ Text("Hello World") .customProgressView(showProgressView: $showProgressView) 成果物 よくあるローディング中画面が表示されています。 動画で見たい場合は、以下のGithubリポジトリで公開しているので、そのREADMEを確認ください。 ColorをProgressViewの前の階層に入れているので、この画面が表示されているときは後ろは操作不可にしてます。 ProgressViewだけでは、後ろは操作不可にしてくれませんので、自分で実装必要でした。 実装を見ればわかりますが、 「読み込み中」のテキストや色、背景色は自由にカスタマイズできます。 実装 まずカスタム ViewModifierの実装です。 import SwiftUI struct CustomProgressView: ViewModifier { @Binding var showProgressView: Bool func body(content: Content) -> some View { // contentはこのカスタムViewModifierを使用する対象Viewのプロキシ ZStack { content if showProgressView { // ProgressViewの背景をタップ不可にするために、Colorを使用 Color.gray.opacity(0.2) VStack(spacing: 6) { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .gray)) .scaledToFit() .frame(width: 22, height: 22) Text("読み込み中") .foregroundColor(Color.gray) .font(.caption2) } } } } } 最初contentを書くのを忘れてしまい、ProgressViewは表示されるが背景にあるはずのViewが全く表示されないという現象で悩んでました。 contentは 修飾する対象のViewのプロキシ だそうなので、これがないと修飾する対象のViewが表示されないのは当然でした。。 なので、当然if文の外にcontentが存在してる必要があります。 そうでないと、showProgressViewがfalseの時には修飾する対象のViewも一緒に非表示になってしまいます。 ちなみにTextのViewを使わなくても、 ProgressView("読み込み")というようにテキストを設定できるのですが、 フォントサイズや色など細かくカスタマイズしたい場合は、TextのViewを自分で作らないといけないみたいです。 ProgressView(Text("読み込み").font(.caption2))というようには書けなかったです。 では次に、ViewのExtensionを実装します。 extension View { func customProgressView(showProgressView: Binding<Bool>) -> some View { self.modifier(CustomProgressView(showProgressView: showProgressView)) } } これで、初めに書いたように .customProgressView(showProgressView: $showProgressView) とModifierをつければ例の画面が表示できるようになります。 Extensionでラップしない場合は .modifier(CustomProgressView(showProgressView: $showProgressView)) と書くことになると思います。(実際にこれで動作確認はしていないのですが動くはず。。) これはこれでいいかもですが、私はラップした方が他の標準Modifierと同じように使えてシンプルかなと思っています。 今更な説明ですが、親View側でshowProgressViewの真偽を切り替えることになると思うので、@Bindingを使っています。 では早速使ってみる。 struct ContentView: View { @ObservedObject private var viewModel = ContentViewModel() var body: some View { VStack(spacing: 50) { Text("テスト1") Text("テスト2") } .background(Color.yellow) .customProgressView(showProgressView: $viewModel.showProgressView) .onAppear { viewModel.onAppear() } } } viewModelクラスのプロパティとして、showProgressViewを@Publishedで持っています。 class ContentViewModel: ObservableObject { @Published var showProgressView = false func onAppear() { showProgressView = true // 3秒後待ってインジケータを削除(実際はAPI通信したり、画像取得したり) DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { self.showProgressView = false } } } 今回は3秒待つだけにしていますが、実際はAPIと通信したりとかとにかく何か時間がかかる処理が実装されることになると思います。 その処理完了の前後でshowProgressViewの真偽を切り替え、その値を監視することによって、 今回のローディング画面の切り替えを行なっています。 参考にした記事 SwiftUIのViewModifierを使ってViewをカスタマイズする 【SwiftUI】カスタムModifierの作成 How to create custom View modifiers for better code reusability in SwiftUI→かなり参考にさせていただきました。ProgressViewではなくもっとUI的に凝ったローディング画面を作りたい場合にはこの記事がおすすめです。 おわりに Custom ViewModifierを勉強する良い機会になりました。 まだまだSwiftUI初心者ですが、ちょっとずつ進歩してることを願ってます。 誤り、もっと良いやり方あるなどありましたら、是非コメントで教えてください 誰かの役に立てば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】よくあるインジケーター表示切り替えを、ProgressViewとカスタム ViewModifierで使いやすくする

はじめに SwiftUIには、ローディング中とかにぐるぐるするインジケーターがProgressViewとして用意されています。 大抵こういうViewは共通化されると思うので、使いやすくするためにProgressViewの カスタム ViewModifierを作ってみました。 普通にViewにしてもいいと思ったのですが、 Viewにすると多分こうなってしまうと思います↓ 使う側では、毎回ZStackで囲わないといけないので、面倒な気がします。 ZStack { Text("Hello World") if showProgressView { CustomProgressView() } } そうではなくこうやって使えた方が、ZStackとか不要だし便利だろうと思ったので、ViewModifierで実装してます。↓ Text("Hello World") .customProgressView(showProgressView: $showProgressView) 成果物 よくあるローディング中画面が表示されています。 動画で見たい場合は、以下のGithubリポジトリで公開しているので、そのREADMEを確認ください。 ColorをProgressViewの前の階層に入れているので、この画面が表示されているときは後ろは操作不可にしてます。 ProgressViewだけでは、後ろは操作不可にしてくれませんので、自分で実装必要でした。 実装を見ればわかりますが、 「読み込み中」のテキストや色、背景色は自由にカスタマイズできます。 実装 まずカスタム ViewModifierの実装です。 import SwiftUI struct CustomProgressView: ViewModifier { @Binding var showProgressView: Bool func body(content: Content) -> some View { // contentはこのカスタムViewModifierを使用する対象Viewのプロキシ ZStack { content if showProgressView { // ProgressViewの背景をタップ不可にするために、Colorを使用 Color.gray.opacity(0.2) VStack(spacing: 6) { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .gray)) .scaledToFit() .frame(width: 22, height: 22) Text("読み込み中") .foregroundColor(Color.gray) .font(.caption2) } } } } } 最初contentを書くのを忘れてしまい、ProgressViewは表示されるが背景にあるはずのViewが全く表示されないという現象で悩んでました。 contentは 修飾する対象のViewのプロキシ だそうなので、これがないと修飾する対象のViewが表示されないのは当然でした。。 なので、当然if文の外にcontentが存在してる必要があります。 そうでないと、showProgressViewがfalseの時には修飾する対象のViewも一緒に非表示になってしまいます。 ちなみにTextのViewを使わなくても、 ProgressView("読み込み")というようにテキストを設定できるのですが、 フォントサイズや色など細かくカスタマイズしたい場合は、TextのViewを自分で作らないといけないみたいです。 ProgressView(Text("読み込み").font(.caption2))というようには書けなかったです。 では次に、ViewのExtensionを実装します。 extension View { func customProgressView(showProgressView: Binding<Bool>) -> some View { self.modifier(CustomProgressView(showProgressView: showProgressView)) } } これで、初めに書いたように .customProgressView(showProgressView: $showProgressView) とModifierをつければ例の画面が表示できるようになります。 Extensionでラップしない場合は .modifier(CustomProgressView(showProgressView: $showProgressView)) と書くことになると思います。(実際にこれで動作確認はしていないのですが動くはず。。) これはこれでいいかもですが、私はラップした方が他の標準Modifierと同じように使えてシンプルかなと思っています。 今更な説明ですが、親View側でshowProgressViewの真偽を切り替えることになると思うので、@Bindingを使っています。 では早速使ってみる。 struct ContentView: View { @ObservedObject private var viewModel = ContentViewModel() var body: some View { VStack(spacing: 50) { Text("テスト1") Text("テスト2") } .background(Color.yellow) .customProgressView(showProgressView: $viewModel.showProgressView) .onAppear { viewModel.onAppear() } } } viewModelクラスのプロパティとして、showProgressViewを@Publishedで持っています。 class ContentViewModel: ObservableObject { @Published var showProgressView = false func onAppear() { showProgressView = true // 3秒後待ってインジケータを削除(実際はAPI通信したり、画像取得したり) DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { self.showProgressView = false } } } 今回は3秒待つだけにしていますが、実際はAPIと通信したりとかとにかく何か時間がかかる処理が実装されることになると思います。 その処理完了の前後でshowProgressViewの真偽を切り替え、その値を監視することによって、 今回のローディング画面の切り替えを行なっています。 参考にした記事 SwiftUIのViewModifierを使ってViewをカスタマイズする 【SwiftUI】カスタムModifierの作成 How to create custom View modifiers for better code reusability in SwiftUI→かなり参考にさせていただきました。ProgressViewではなくもっとUI的に凝ったローディング画面を作りたい場合にはこの記事がおすすめです。 おわりに Custom ViewModifierを勉強する良い機会になりました。 まだまだSwiftUI初心者ですが、ちょっとずつ進歩してることを願ってます。 誤り、もっと良いやり方あるなどありましたら、是非コメントで教えてください 誰かの役に立てば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CAGradientLayer( ) を少し使ってみました。

今回の内容 機能説明 Timer( )を使って、一定秒後にランダムに背景色が変わってるように見せる。 全体コード import UIKit class ViewController: UIViewController { var gradient = CAGradientLayer() override func viewDidLoad() { super.viewDidLoad() gradient.frame = CGRect(x: self.view.frame.minX, y: self.view.frame.minY, width: self.view.frame.size.width, height: self.view.frame.size.height) Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(changeGradient), userInfo: nil, repeats: true) } @objc func changeGradient(){ UIView.animate(withDuration: 1,delay: 0,options: .allowUserInteraction ,animations: {[self] in gradient.colors = [ UIColor(red: CGFloat.random(in: 0...255) / 255, green:CGFloat.random(in:0...255) / 255, blue: CGFloat.random(in: 0...255) / 255, alpha: CGFloat.random(in: 0.0...1.0)).cgColor, UIColor(red: CGFloat.random(in: 0...255) / 255, green:CGFloat.random(in: 0...255) / 255, blue: CGFloat.random(in: 0...255) / 255, alpha: CGFloat.random(in: 0.0...1.0)).cgColor ] gradient.startPoint = CGPoint(x: 0.5, y: 0.5) gradient.endPoint = CGPoint(x: 0.5, y: 0) self.view.layer.insertSublayer(gradient, at: 0) },completion: nil) } } 終わり 間違いの指摘、ご質問などありましたら、コメントまでお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS13 / iOS14 /iOS15 で、AppDelegateのライフサイクルは違うよ 14がおかしいんだけど

以前書いた記事 で、OSによってコロコロ変わるライフサイクルに苦しめられてることをぼやかずに分析だけしたのですが、iOS15でもまたまたライフサイクルが変わっています。いい加減にしてほしい! 結論から言うと、iOS14だけおかしかったのです。iOS13もか。 iOS15では、iOS12以前のライフサイクルに戻っているようです。 iOS15 -[AppDelegate application:didFinishLaunchingWithOptions:] -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[ViewController viewDidAppear:] -[AppDelegate applicationDidBecomeActive:] -[ViewController applicationDidBecomeActiveNotification:] iOS14(iOS14.5) -[AppDelegate application:didFinishLaunchingWithOptions:] -[AppDelegate applicationDidBecomeActive:] -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[ViewController viewDidAppear:] viewDidLoadとかで applicationDidBecomeActiveNotification へのObserverを貼ったところで、既にapplicationDidBecomeActiveは呼ばれているので、初回はapplicationDidBecomeActiveNotificationを受け取れない! iOS13 -[AppDelegate application:didFinishLaunchingWithOptions:] -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[AppDelegate applicationDidBecomeActive:] -[ViewController applicationDidBecomeActiveNotification:] -[ViewController viewDidAppear:] iOS11 / iOS12 -[AppDelegate application:didFinishLaunchingWithOptions:] -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[ViewController viewDidAppear:] -[AppDelegate applicationDidBecomeActive:] -[ViewController applicationDidBecomeActiveNotification:]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Text Fieldのなんやかんやをカスタムした

こんなTextFieldを実装したのでメモしとく 入力カーソル:無効 範囲選択:無効 コピー・ペースト・選択:無効(吹き出しが出ないようにする) CustomTextField.swift import UIKit class CustomTextField: UITextField { // 入力カーソル:無効 override func caretRect(for position: UITextPosition) -> CGRect { return CGRect.zero } // 範囲選択:無効 func selectionRects(for range: UITextRange) -> [Any] { return [] } // コピー・ペースト・選択:無効 override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { return false } } ちなみに、、、 CustomTextField.swift open var caretFlag: Bool = false // 入力カーソル:無効、有効の切り替え override func caretRect(for position: UITextPosition) -> CGRect { if caretFlag { return super.caretRect(for: position) } else { return CGRect.zero } } こうすることでcaretFlagがtrueならカーソル非表示、falseならカーソル表示に切り替えが可能です。 Storyboardの場合はCustomTextFieldをクラスとして指定してやれば実装完了です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NCMBのSwift SDKとWebSocketを使ってチャットアプリを作る(その3:チャットの表示とチャットデータの取得)

NCMBのSwift SDKを使ってデモアプリを作ってみます。リアルタイム通信系は人気があるのですが、NCMBでは残念ながらWebSocketは使えません。そこで今回はPieSocketというWebSocketを提供するサービスと組み合わせて、Swift製のチャットアプリを作ってみます。 前回はチャットメッセージの送信について解説しました。今回はチャットメッセージの表示と、既存データの取得について解説します。 コードについて 今回のコードはNCMBMania/Swift_Chat_Demoにアップロードしてあります。実装時の参考にしてください。 チャットメッセージの表示 チャットメッセージの表示は ChatView にて行います。WebSocketの接続やメッセージの送受信を管理している ChatScreenModel を ObservedObject として定義しています。 struct ChatView: View { @ObservedObject var chat = ChatScreenModel() そして chat.messages にメッセージが追加されたらリストを更新しています。この時、ForEach の id は objectId になります。 ScrollView { ScrollViewReader { proxy in LazyVStack(spacing: 8) { ForEach(chat.messages, id: \.objectId ) { message in ChatMessageRow(message: message) } } .onChange(of: chat.messages.count) { _ in scrollToLastMessage(proxy: proxy) } } } scrollToLastMessage はリストの最後のメッセージを追いかける形で自動スクロールするためのメソッドです。 // 自動スクロール用 private func scrollToLastMessage(proxy: ScrollViewProxy) { if let lastMessage = chat.messages.last { withAnimation(.easeOut(duration: 0.4)) { proxy.scrollTo(lastMessage.objectId, anchor: .bottom) } } } 自分と相手のメッセージで表示を分ける 自分のメッセージか否かで表示(色や配置)を分けています。これは ChatMessageRow で実装しています。自分のメッセージかどうかはNCMBObjectにあるuserId(チャット投稿者のobjectId)を使って判定しています。 struct ChatMessageRow: View { @State var message: NCMBObject // 日付のフォーマット(時刻のみ) static private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .none formatter.timeStyle = .short return formatter }() var body: some View { HStack { if isMe() { Spacer() } VStack(alignment: .leading, spacing: 6) { HStack { Text(message["displayName"]! as String) .fontWeight(.bold) .font(.system(size: 12)) Text(Self.dateFormatter.string(from: createDate())) .font(.system(size: 10)) .opacity(0.7) } Text(message["body"]! as String) } .foregroundColor(isMe() ? .white : .black) .padding(10) .background(isMe() ? Color.blue : Color(white: 0.95)) .cornerRadius(5) if !isMe() { Spacer() } } } // 自分宛かどうか判定する関数 func isMe() -> Bool { if let userId: String = message["userId"] { let user = NCMBUser.currentUser return userId == user!.objectId } return false } // 日付をフォーマットに沿って返す func createDate() -> Date { if let createDate: String = message["createDate"] { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" return dateFormatter.date(from: createDate)! } return Date() } } 既存メッセージの取得 過去にやり取りされたメッセージは NCMB のChatクラスにあります。チャット画面が表示されたタイミングで、そのデータを取得します。データを受け取ったらメインスレッドで更新する必要があるので注意してください。メッセージはそのまま chat.messages に入れてしまえば、表示に反映されます。 // NCMBに保存されているメッセージを取得する関数 func getPastMessages() { // データ取得用のクエリオブジェクトを用意 var query = NCMBQuery.getQuery(className: "Chat") // 並び順はcreateDateの昇順 query.order = ["createDate"] // 20件取得 query.limit = 20 // 検索実行 query.findInBackground(callback: { result in // 結果判定 switch result { case let .success(ary): // 取得できた場合は結果をチャットメッセージとして反映 DispatchQueue.main.async { chat.messages = ary } break case .failure(_): break // エラーの場合 } }) } 反映をリアルタイムで行う メッセージが追加された際に表示への反映をリアルタイムで行うため ChatScreenModel の messages に @Published を付けておきます。 final class ChatScreenModel: ObservableObject { private var webSocketTask: URLSessionWebSocketTask? @Published var messages: [NCMBObject] = [] これで過去のメッセージも表示できるようになります。 まとめ 今回はNCMBの匿名認証とデータストア、そしてPieSocketを使ってチャットアプリを作りました。今回はチャンネルIDを1固定にしていますが、これを変えることで複数のチャットルームにも対応します。ぜひカスタマイズして、Swiftアプリの中にチャット機能を組み込んでください。 ドキュメント : 開発者向けドキュメント | ニフクラ mobile backend
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NCMBのSwift SDKとWebSocketを使ってチャットアプリを作る(その2:メッセージの送信とデータストアへの保存)

NCMBのSwift SDKを使ってデモアプリを作ってみます。リアルタイム通信系は人気があるのですが、NCMBでは残念ながらWebSocketは使えません。そこで今回はPieSocketというWebSocketを提供するサービスと組み合わせて、Swift製のチャットアプリを作ってみます。 前回は仕様と画面の説明、Swift SDKの初期化までを説明しました。今回はチャットメッセージの送信について解説します。 コードについて 今回のコードはNCMBMania/Swift_Chat_Demoにアップロードしてあります。実装時の参考にしてください。 チャットメッセージの送信 フローは次のようになります。 NCMBのデータストアにチャットメッセージを保存する WebSocketでメッセージを送信する NCMBのデータストアにチャットメッセージを保存する 入力されたテキストと、ログインユーザの情報を紐付けてNCMBに保存します。これは ChatInputView の send メソッドに実装します。注意点としてはACLを使ってアクセスコントロールを設定しているところでしょうか。 // チャットメッセージを送信する関数 func send() { // NCMBのデータストア用のオブジェクトを用意 let obj = NCMBObject(className: "Chat") // 必要なデータを設定 obj["body"] = message let user = NCMBUser.currentUser obj["userId"] = user!.objectId var acl = NCMBACL.empty // ACL(アクセス管理)を定義 // * = 全員。読み込み可能、編集不可 acl.put(key: "*", readable: true, writable: false) // ユーザのobjectId。指定したユーザ(投稿者)のみ読み込み可能、編集可能 acl.put(key: user!.objectId!, readable: true, writable: true) obj.acl = acl // ACLを設定 // 表示名も設定 if let displayName: String = user!["displayName"] { obj["displayName"] = displayName } _ = obj.save() // 保存 message = "" // 入力されていたメッセージを消す chat.send(obj: obj) // WebSocketで送信 } WebSocketでメッセージを送信する 保存後、WebSocketでメッセージを送信します。WebSocketでは基本的にテキストメッセージを送信するので、NCMBObjectから必要なデータを取り出して、JSONテキストに変換します。これは ChatScreenModel の makeMessage メソッドで行います。 // チャットメッセージを送信する関数 func send(obj: NCMBObject) { // NCMBObjectからメッセージを作成して、送信 webSocketTask?.send(.string(makeMessage(obj: obj)), completionHandler: { error in if error != nil { // エラーの場合 print(error) } }) } makeMessage は必要な情報を集めてDictionaryを作成し、それをJSONテキスト化します。日付だけISO8601形式に変換して入れています。 // NCMBObjectをDictionaryにして、JSON文字列にする関数 private func makeMessage(obj: NCMBObject) -> String { // Dictionaryの準備 var json = Dictionary<String, String>() json["objectId"] = obj.objectId! // チャットメッセージのobjectId json["body"] = obj["body"] ?? "" // チャットメッセージ json["userId"] = obj["userId"] ?? "" // チャットの送信者 json["displayName"] = obj["displayName"] ?? "" // チャットの表示名 // 投稿日時 let formatter = ISO8601DateFormatter() json["createDate"] = formatter.string(from: Date()) do { // Dictionaryを文字列化 let jsonData = try JSONSerialization.data(withJSONObject: json) return String(bytes: jsonData, encoding: .utf8)! } catch (let e) { print(e) } return "" // エラーの場合 } WebSocketでメッセージを送信すると受信時のハンドラである onReceive メソッドが呼ばれます。ここでは送信時と逆に、受け取ったメッセージを分解してNCMBObjectオブジェクトを作成します。なお、URLSessionWebSocketTaskではメッセージを1度しか受信してくれないので、最後にもう一度受信時のハンドラを設定しておきます。 // メッセージを受け取った時に呼ばれる関数 private func onReceive(incoming: Result<URLSessionWebSocketTask.Message, Error>) { switch incoming { case let .success(message): // 正しく受け取れている場合 // メッセージの種類に応じて処理分け(今回はテキストのみ) switch message { case let .string(msg): // テキストをデータ化 let data: Data = msg.data(using: String.Encoding.utf8)! do { // JSONとしてパース let params = try JSONSerialization.jsonObject(with: data) as! Dictionary<String, String> // NCMBObjectとして作り直し let obj = NCMBObject(className: "Chat") obj["displayName"] = params["displayName"] obj.objectId = params["objectId"] obj["userId"] = params["userId"] obj["body"] = params["body"] obj["createDate"] = params["createDate"] DispatchQueue.main.async { self.messages.append(obj) // メッセージの配列に追加 } } catch { } break case let .data(data): print(data) @unknown default: print("unknown \(message)") } break case let .failure(err): print(err) break } webSocketTask?.receive(completionHandler: onReceive) } これでメッセージの送信処理が完了します。 まとめ 今回はチャットメッセージの送信と、NCMBのデータストアへの保存について解説しました。次回はチャットメッセージの表示と、過去のメッセージを取得する部分を解説します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NCMBのSwift SDKとWebSocketを使ってチャットアプリを作る(その1:画面の説明とSDKの導入)

NCMBのSwift SDKを使ってデモアプリを作ってみます。リアルタイム通信系は人気があるのですが、NCMBでは残念ながらWebSocketは使えません。そこで今回はPieSocketというWebSocketを提供するサービスと組み合わせて、Swift製のチャットアプリを作ってみます。 今回は画面の説明とSDKの導入までを進めます。 コードについて 今回のコードはNCMBMania/Swift_Chat_Demoにアップロードしてあります。実装時の参考にしてください。 利用技術について 今回は次のような組み合わせになっています。 Swift 5.4 Xcode 12.5.1 NCMB PieSocket 仕様について PieSocketはWebSocketだけを提供するので、データを保存しません。そのためWebSocketにつながっている時のメッセージは画面に表示できますが、立ち上げ直すとすべてのメッセージが消えてしまいます。そこで、NCMBのデータストアにメッセージを保存して、アプリを立ち上げた時にメッセージを再現できるようにします。 利用する機能について チャットアプリで利用するNCMBの機能は次の通りです。 認証機能 匿名認証 データストア チャットクラスへのデータ登録、一覧表示 画面について 今回はSwiftUIを以下の5つのViewに分けています。 ContentView NameView ChatView ChatMessageRow ChatInputView ContentView ログイン状態に応じて表示を分けています。 import SwiftUI import NCMB struct ContentView: View { private var user = NCMBUser.currentUser @State private var displayName = "" var body: some View { VStack { if displayName != "" { // 表示名が設定されている場合 ChatView() } else { // 表示名がない場合 NameView(displayName: $displayName) } }.onAppear() { setDisplayName() } } // 表示名を設定する関数 func setDisplayName() -> Void { if let name: String = user!["displayName"] { displayName = name } } } NameView チャット用の表示名を設定するViewです。 import SwiftUI import NCMB struct NameView: View { @Binding var displayName: String @State var name = "" var body: some View { VStack { TextField("お名前", text: $name) // 2 .padding(10) .background(Color.secondary.opacity(0.2)) .cornerRadius(5) Button("登録する", action: { update() }) } } // 設定された表示名をNCMBの認証データに保存する関数 func update() { let user = NCMBUser.currentUser user?["displayName"] = name _ = user?.save() displayName = name } } ChatView チャットの表示を担当するViewです。 import SwiftUI import NCMB struct ChatView: View { @ObservedObject var chat = ChatScreenModel() var body: some View { VStack { ScrollView { ScrollViewReader { proxy in LazyVStack(spacing: 8) { ForEach(chat.messages, id: \.objectId ) { message in ChatMessageRow(message: message) } } .onChange(of: chat.messages.count) { _ in scrollToLastMessage(proxy: proxy) } } } ChatInputView(chat: chat) } .onAppear() { chat.connect() getPastMessages() } } // NCMBに保存されているメッセージを取得する関数 func getPastMessages() { // 2回目以降の記事で記述 } // 自動スクロール用 private func scrollToLastMessage(proxy: ScrollViewProxy) { if let lastMessage = chat.messages.last { // 4 withAnimation(.easeOut(duration: 0.4)) { proxy.scrollTo(lastMessage.objectId, anchor: .bottom) // 5 } } } } ChatMessageRow チャットメッセージの一行分の表示を行うViewです。 import SwiftUI import NCMB struct ChatMessageRow: View { @State var message: NCMBObject // 日付のフォーマット(時刻のみ) static private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .none formatter.timeStyle = .short return formatter }() var body: some View { HStack { if isMe() { Spacer() } VStack(alignment: .leading, spacing: 6) { HStack { Text(message["displayName"]! as String) .fontWeight(.bold) .font(.system(size: 12)) Text(Self.dateFormatter.string(from: createDate())) .font(.system(size: 10)) .opacity(0.7) } Text(message["body"]! as String) } .foregroundColor(isMe() ? .white : .black) .padding(10) .background(isMe() ? Color.blue : Color(white: 0.95)) .cornerRadius(5) if !isMe() { Spacer() } } } // 自分宛かどうか判定する関数 func isMe() -> Bool { if let userId: String = message["userId"] { let user = NCMBUser.currentUser return userId == user!.objectId } return false } // 日付をフォーマットに沿って返す func createDate() -> Date { if let createDate: String = message["createDate"] { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" return dateFormatter.date(from: createDate)! } return Date() } } ChatInputView チャットのメッセージ入力を担当するViewです。 import SwiftUI import NCMB struct ChatInputView: View { @State private var message = "" @State var chat: ChatScreenModel var body: some View { HStack { TextField("Message", text: $message) .padding(10) .background(Color.secondary.opacity(0.2)) .cornerRadius(5) Button(action: { send() }) { Image(systemName: "arrowshape.turn.up.right") .font(.system(size: 20)) } .padding() .disabled(message.isEmpty) } .padding() } // チャットメッセージを送信する関数 func send() { // 2回目以降の記事で解説 } } キーの管理 今回は3つのキーを利用します。 NCMBのアプリケーションキー NCMBのクライアントキー PieSocketのアプリケーションキー コード中に直書きするのは良くないので、Property Listを使います。APIKey.plistというファイルを作成して、その中に3つのキーを定義します。 PieSocketApiKey ApplicationKey ClientKey それぞれStringで、キーを入力します。 そしてそのファイルを扱うKeyManagerを作成します。こちらの内容はAPI keyを.plistにStringで保存して隠す(例: Google Maps Api) - Qiitaを参考にさせてもらいました。 import Foundation struct KeyManager { private let keyFilePath = Bundle.main.path(forResource: "APIKey", ofType: "plist") func getKeys() -> NSDictionary? { guard let keyFilePath = keyFilePath else { return nil } return NSDictionary(contentsOfFile: keyFilePath) } func getValue(key: String) -> AnyObject? { guard let keys = getKeys() else { return nil } return keys[key]! as AnyObject } } このクラスを作っておけば KeyManager().getValue(key: "ApplicationKey") as! String のようにしてキーを扱えるようになります。  WebSocket用のクラスの作成 ChatScreenModel.swiftというファイルを作成します。この内容は A simple chat app with SwiftUI and WebSockets — or: Swift in the back, Swift in the front! | by Freek Zijlmans | Medium を参考にしています。大枠としては次のようになります。メッセージを保存するため、 messages はNCMBObject(NCMBのデータ保存用オブジェクト)の配列となっています。 import Combine import Foundation import NCMB final class ChatScreenModel: ObservableObject { private var webSocketTask: URLSessionWebSocketTask? @Published var messages: [NCMBObject] = [] // WebSocket(今回はPieSocket)への接続を行います func connect() { let channelId = "1" let url = URL(string: "wss://free3.piesocket.com/v3/\(channelId)?api_key=\(KeyManager().getValue(key: "PieSocketApiKey")!)&notify_self")! webSocketTask = URLSession.shared.webSocketTask(with: url) // メッセージを受け取った時に呼ばれるハンドラ webSocketTask?.receive(completionHandler: onReceive) webSocketTask?.resume() } // 接続解除時に実行する関数 func disconnect() { webSocketTask?.cancel(with: .normalClosure, reason: nil) } // メッセージを受け取った時に呼ばれる関数 private func onReceive(incoming: Result<URLSessionWebSocketTask.Message, Error>) { // 2回目以降の記事で解説 } // NCMBObjectをDictionaryにして、JSON文字列にする関数 private func makeMessage(obj: NCMBObject) -> String { // 2回目以降の記事で解説 } // チャットメッセージを送信する関数 func send(obj: NCMBObject) { // NCMBObjectからメッセージを作成して、送信 webSocketTask?.send(.string(makeMessage(obj: obj)), completionHandler: { error in if error != nil { // エラーの場合 print(error) } }) } } 今回のプロジェクト 今回は言語がSwift、インタフェースがSwiftUI、ライフサイクルはSwiftUI Appとしています。 SDKのインストール FileメニューからSwift Packages > Add Package Dependencyと選択します。 出てきたダイアログでSwift SDKのGitリポジトリURLを入力します。GitHubのリポジトリでHTTPSとして取得できるもの、または下記URLになります。 https://github.com/NIFCLOUD-mbaas/ncmb_swift.git バージョンは最新のものでかまいません。 後はFinishボタンを押せば完了です。 初期化 今回はSwiftUIを利用しています。ライフサイクルもSwiftUIです。 まずSDKをインポートします。 import SwiftUI import NCMB 次に scenePhase を追加します。 @main struct ChatApp: App { // 追加 @Environment(\.scenePhase) private var scenePhase 後は body 内で onChange を使って初期化します。 var body: some Scene { WindowGroup { ContentView() }.onChange(of: scenePhase) { scene in switch scene { case .active: // キーの設定 let applicationKey = KeyManager().getValue(key: "ApplicationKey") as! String let clientKey = KeyManager().getValue(key: "ClientKey") as! String // 初期化 NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey) case .background: break case .inactive: break default: break } } } 匿名認証 今回は匿名認証(ID、パスワードを使わない、デバイス固有に生成したUUIDを使った認証)を利用します。そこで、初期化した後にSwift SDKの匿名認証を有効にします。 NCMBUser.enableAutomaticUser() 認証状態の確認 次に認証状態を確認する checkAuth 関数を呼び出します。内容は次の通りです。 NCMBUser.currentUser が nil の場合は認証されていないので、匿名認証を実行します。 func checkAuth() -> Void { // 認証データがあれば処理は終了 if NCMBUser.currentUser != nil { return; } // 匿名認証実行 _ = NCMBUser.automaticCurrentUser() } セッションの有効性チェック 認証されている場合でも、セッションの有効性は確認していません。ローカルにある認証データを復元している状態のためです。そこで、データストアに一度アクセスを行い、API通信の有効性を確認します。 func checkSession() -> Bool { var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo") query.limit = 1 // レスポンス件数を最小限にする // アクセス let results = query.find() // 結果の判定 switch results { case .success(_): break case .failure(_): // 強制ログアウト処理 _ = NCMBUser.logOut() return false } return true } 上記コードでログアウト処理を実行していますが、これは必ず失敗します。セッションが無効になっているため、ログアウトAPIの実行もまた、失敗するためです。そこで NCMB/NCMBUser.swift を開いて logOutInBackground を次のように修正します。 public class func logOutInBackground(callback: @escaping NCMBHandler<Void>) -> Void { NCMBLogoutService().logOut(callback: {(result: NCMBResult<NCMBResponse>) -> Void in // レスポンスに関係なくログアウト処理 deleteFile() _currentUser = nil switch result { case .success(_): callback(NCMBResult<Void>.success(())) break case let .failure(error): callback(NCMBResult<Void>.failure(error)) break } }) } 最終的にChatAppの内容は次のようになります。 struct ChatApp: App { @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() } .onChange(of: scenePhase) { scene in switch scene { case .active: // キーの設定 let applicationKey = KeyManager().getValue(key: "ApplicationKey") as! String let clientKey = KeyManager().getValue(key: "ClientKey") as! String // 初期化 NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey) NCMBUser.enableAutomaticUser() checkAuth() if checkSession() == false { checkAuth() } case .background: break case .inactive: break default: break } } } func checkAuth() -> Void { // 認証データがあれば処理は終了 if NCMBUser.currentUser != nil { return; } // 匿名認証実行 _ = NCMBUser.automaticCurrentUser() } // セッションの有効性をチェックする関数 func checkSession() -> Bool { var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo") query.limit = 1 // レスポンス件数を最小限にする // アクセス let results = query.find() // 結果の判定 switch results { case .success(_): break case .failure(_): // 強制ログアウト処理 _ = NCMBUser.logOut() return false } return true } } NCMBの管理画面を修正 最後にNCMBの管理画面でアプリ設定を開き、匿名認証を有効にします。 これでSwift SDKの初期化と匿名認証処理が完了になります。 まとめ 今回はチャットアプリの仕様と画面、NCMBの初期化までを解説しました。次はチャットメッセージの送信と、NCMBのデータストアへの保存について解説します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】QRコードの分割情報を取得する

iOSアプリでQRコードを読み込もうとしたら、QRコードが分割されている場合があることを知りました。 お薬手帳や車検証などにも使われているみたいです。 iOSアプリでその分割情報を取得する「これだ!」って方法が見つからなかったのでまとめておきます。 「いやいや、もっといい方法があるよ」という方はコメントいただけると助かります。。。 分割QRコードについて QRコードがなんぞやっていうのは割愛します。 ググってくださいませ。 今回伝えたいのは、QRコードは分割されることがあるということです。 上記のような2つのお薬手帳用のQRコードは2つ揃って必要な情報を取得できます。 1つのQRコードが保持できる情報量は決まっているため、 上限を超える情報量を出力するためにQRコードは分割できるって話です。 分割情報はどこにある? QRコードをバイナリにすると分かります。 バイナリ情報 説明 1バイト目の最初4ビット 3だと分割QRコード 1バイト目の後ろ4ビット 分割されているQRコードの位置(0開始) 2バイト目の最初4ビット 分割されているQRコードの最大位置 2バイト目の後ろ4ビット パリティ1 3バイト目の最初4ビット パリティ2 はい、シンプルですね。 見ての通りですが、分割情報が4ビットしかないので最大16分割までしかできません。 例 ここに「00110000 00011001 00110010~」で始まるQRコードがあるとします。 16進数にすると「30 19 32~」となります。 先程の表に照らし合わせると、 1バイト目の最初4ビットが3なので、このQRコードは分割されていると分かります。 1バイト目の後ろ4ビットが0なので、分割QRの1番目だと分かります。(0スタートなので1番目です) 2バイト目の最初4ビットが1なので、QRコードの総数は2つだと分かります。 2バイト目の後ろ4ビットと3バイト目の最初4ビットは、パリティです。2つ目のQRコードと同じになるのでIDっぽく使えます。 ちなみに、上記のQRコードの2つ目のバイナリ(16進表記)は以下の通りとなります。 「31 19 34~」 Swiftでどう取得するのか? AVFoundationでQRコードを読み取るときのデリゲートメソッド内に取得方法を記載します。 func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { if let metadataObject = metadataObjects.first { guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } guard let readableObjectForQRCode = readableObject.descriptor as? CIQRCodeDescriptor else { return } // 16進数のQRコード情報を取得 let hexQRCodeData = readableObjectForQRCode.errorCorrectedPayload.map { String(format: "%02X", $0)} // 分割コードかチェック(trueなら分割コード) let isSeparatedQRCode = hexQRCodeData.first!.hasPrefix("3") // 分割コードの総数を取得 ​​​​​​​​let secondByteFirstHex = hexQRCodeData[1].prefix(1) + 1 ​​​​​​​​let total = Int(secondByteFirstHex, radix: 16)! // 分割コードの位置を取得 let ​​​​firstByteSecondHex = hexQRCodeData[0].suffix(1) let position = Int(firstByteSecondHex, radix: 16)! } } AVFoundationでQRコードを読み取る方法は以下記事を参考 https://qiita.com/ikaasamay/items/58d1a401e98673a96fd2 https://dev.classmethod.jp/articles/ios-avfoundation/ https://dev.classmethod.jp/articles/ios-avfoundation-avcapturemetadataoutput-qrcode/ まとめ そのうちAppleがいい感じのメソッドを提供しそうな気もする。 でも、とりあえずQRコードの仕様が分かればそんな難しくないので別に不要な気もする。 いずれにせよ困っている人の参考になれば幸い! 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift デコレータパターン

デコレータパターン The Decorator(デコレータ)とはデザインパターンの一つ。 既存のクラスに存在しなかった追加機能を実装するために用いる。クラス内のゴニョゴニョせずに追加機能を実装できるデザインパターンと覚えれば良さげ、、 extension extensionがあるから必要なくない? よくextensionをつかって関数を作成したり、Delegateパターンを追加するのが王道な気がする。。 可読性も上がるし、実際僕自身もたくさん使用する。 ただ、既存のクラスをあくまで拡張するのがextensionなのでoverrideはできないし、ストアドプロパティをゴニョゴニョすることはできない。 例コード // 全ての親クラス class Living { func bark() -> String { return "live" } } // 動物クラス class Animal: Living { override func bark() -> String { return "animal" } } // 各動物の種類の親クラス class Kind: Living { let living: Living init(living: Living) { self.living = living } } //?クラス class Dog: Kind { override func bark() ->String { return "wanwan" } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】シングルトン(Singleton)パターン

はじめに コードレビューをしていただいた際シングルトンを教えていただき、コードがかなりスッキリしたので備忘録として書きます! シングルトンパターンとは シングルトンパターンとは、あるクラスのインスタンスの生成を一つに制限する設計のことをいいます。 どのようなケースで使うのか アプリケーション全体で統一しなければならない仕組みを実装する際に使用されます。 例えば、インスタンスに共通の値を保持させたい場合、画面遷移やAPI、ログイン機能等のロジックを集約したい場合などに使用します。 使用方法 final class Singleton { // 初期化 static var singleton: Singleton = .init() // 外部からのイニシャライズを防ぐ private init() {} 最後に シングルトンパターンは共通化する分、デメリットも多く存在しますが、適切な場面で使うことで便利に使用することができます!(随時更新予定)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftでフィボナッチ数列

Playgroundsで遊んでみた。 func fib(n:Int64) -> Int64 { if n==0 { return 0 }else if n==1{ return 1 } return fib(n:n-1) + fib(n:n-2) } print(fib(n:20)) func fib2(n:Int64) -> (Int64, Int64) { if n==0 { return (0, 1) } let (x, y) = fib2(n:n-1) return (y, x+y) } print(fib2(n:20).0) func fib3(n:Int64) -> ((Int64, Int64), (Int64, Int64)) { if n == 1 { return ((0, 1), (1, 1)) }else if n%2==0 { let ((a, b), (c, d)) = fib3(n:n/2) return ((a*a+b*c, b*(a+d)), (c*(a+d), d*d+b*c)) }else{ let ((a, b), (c, d)) = fib3(n:n-1) return ((b,a+b), (d,c+d)) } } print(fib3(n:20).0.1) 実行回数がリアルタイムで見れるのが便利。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む