- 投稿日:2020-08-18T23:56:12+09:00
APIKitなどのライブラリを使わずに標準のURLSessionで通信処理を実装をする
✔️実装パターン①
RequestとResponseの関係を一対一にする。
Protocolでリクエストを作成
import Foundation protocol APIRequest { associatedtype Responses: Decodable associatedtype Parameters: Encodable var path: String { get } var method: String { get } var headers: String? { get } var queries: [URLQueryItem]? { get set } var body: Parameters? { get set } } struct Request { struct Login: APIRequest { typealias Responses = UserResponse typealias Parameters = UserRequest let path = "/login" let method = "POST" let headers: String? = nil var queries: [URLQueryItem]? var body: UserRequest? } struct Signup: APIRequest { typealias Responses = UserResponse typealias Parameters = UserRequest let path = "/sign_up" let method = "POST" let headers: String? = nil var queries: [URLQueryItem]? var body: UserRequest? } }UserRequest
import Foundation struct UserRequest: Encodable { var email: String var password: String }UserResponse
import Foundation struct UserResponse: Decodable { var result: User struct User: Decodable { var id: Int var email: String var token: String } }APIClient
APIRequestに準拠している構造体を引数に入れる。
typeによって処理を切り替える。import Foundation struct APIClient { static func sendRequest<T: APIRequest>( from type: T, completion: @escaping (Result<T.Responses, Error>) -> Void) { let BaseURL: String = "http://xxxxxxxxx" func createRequest() -> URLRequest? { guard var components = URLComponents(string: "\(BaseURL)\(type.path)") else { return nil} if type.method == "GET" { let queryItems = type.queries components.queryItems = queryItems } guard let url = components.url else { return nil} var request = URLRequest(url: url) request.httpMethod = type.method if type.method != "GET" { let httpBody = JSONEncoder().encode(value: type.body) request.httpBody = httpBody } request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(type.headers, forHTTPHeaderField: "access_token") return request } guard let request = createRequest() else { return } let task = URLSession.shared.dataTask(with: request) { (data, res, error) in if let error = error { completion(.failure(error)) } guard let data = data else { print("no data") return } guard let res = res as? HTTPURLResponse, (200...299).contains(res.statusCode) else { return } do { let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase let json = try jsonDecoder.decode(T.Responses.self, from: data) DispatchQueue.main.sync { completion(.success(json)) } } catch { DispatchQueue.main.sync { completion(.failure(error)) } } } task.resume() } }呼び出し元
import UIKit class ViewController: UIViewController { typealias UserRequest = Request.Login override func viewDidLoad() { super.viewDidLoad() let params = UserRequest.Parameters( email: "xxx.com", password: "xxxxx" ) let queryItems: [URLQueryItem] = { var queryItems = [URLQueryItem]() queryItems.append(URLQueryItem(name: "email", value: params.email)) queryItems.append(URLQueryItem(name: "password", value: params.password)) return queryItems }() APIClient.sendRequest(from: UserRequest(queries: queryItems)) { (result) in switch result { case .success(let response): print("success", response) case .failure: print("failure") } } } }extension
import Foundation extension JSONEncoder { func encode<T: Encodable>(value: T) -> Data? { self.keyEncodingStrategy = .convertToSnakeCase let encodeValue = try? self.encode(value) return encodeValue } }✔️実装パターン② アンチパターン
最初は以下のように書いていたのですが、実はアンチパターンのようでした。
理由としては以下の通りです。
- Requestに対して引数にResponseの型を入れる。
- リクエストをする際にリクエスト・レスポンスを入力する必要があると、間違えて入力した際にレスポンスが受け取れなくなる、なのでリクエストとレスポンスは一対一にするのがよさそう。
URLRouter
URLSessionの作成、リクエストを作成する。
// URLの向き先を作成する型、エンドポイントを追加したい時やHTTPの設定はこの型を参照する。 enum URLRouter { case addBook case editBook(id: Int, body: [String: Any]) private static let baseURLString = "YOUR_BASE_URL_STRING" private enum HTTPMethod { case get case post case put case delete var value: String { switch self { case .get: return "GET" case .post: return "POST" case .put: return "PUT" case .delete: return "DELETE" } } } private var method: HTTPMethod { switch self { case .addBook: return .get case .editBook: return .put } } private var path: String { switch self { case .addBook: return "/books" case .editBook(let id): return "/books/\(id)" } } func request() throws -> URLRequest { let urlString = "\(URLRouter.baseURLString)\(path)" guard let url = URL(string: urlString) else { throw ErrorType.parseUrlFail } var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 10) request.httpMethod = method.value request.addValue("application/json", forHTTPHeaderField: "Content-Type") switch self { case .addBook: return request case .editBook(_, let body): request.httpBody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) return request } } }ネットワーク通信をするクラス
class Network { static let shared = Network() private let config: URLSessionConfiguration private let session: URLSession private init() { config = URLSessionConfiguration.default session = URLSession(configuration: config) } // リクエスト時にURLRouterを注入する。 func request<T: Decodable>(router: URLRouter, completion: @escaping (Result<T, Error>) -> ()) { do { let task = try session.dataTask(with: router.request()) { (data, urlResponse, error) in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let statusCode = urlResponse?.getStatusCode(), (200...299).contains(statusCode) else { let errorType: ErrorType switch urlResponse?.getStatusCode() { case 404: errorType = .notFound case 422: errorType = .validationError case 500: errorType = .serverError default: errorType = .defaultError } completion(.failure(errorType)) return } guard let data = data else { completion(.failure(ErrorType.defaultError)) return } do { let result = try JSONDecoder().decode(T.self, from: data) completion(.success(result)) } catch let error { completion(.failure(error)) } } } task.resume() } catch let error { completion(.failure(error)) } } } extension URLResponse { func getStatusCode() -> Int? { if let httpResponse = self as? HTTPURLResponse { return httpResponse.statusCode } return nil } } enum ErrorType: LocalizedError { case parseUrlFail case notFound case validationError case serverError case defaultError var errorDescription: String? { switch self { case .parseUrlFail: return "Cannot initial URL object." case .notFound: return "Not Found" case .validationError: return "Validation Errors" case .serverError: return "Internal Server Error" case .defaultError: return "Something went wrong." } } }呼び出し元
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let id = 2525 // クエリ let params: [String: Any] = [ "first_name": "Amitabh2", "last_name": "Bachchan2", "email": "ab@bachchan.com", "phone_number": "+919980123412", "favorite": false ] Network.shared.request(router: .editBook(id: id, body: params)) { (result: Result<BookEditResponse, Error>) in switch result { case .success(let item): print("成功") case .failure(let err): print("失敗") } } } }少しでも参考になりましたら幸いです?
参考
- 投稿日:2020-08-18T23:48:20+09:00
【Flutter】画面サイズに応じて文字サイズを自動で適用する方法
この記事を読んで習得できること
画面サイズに応じて文字サイズを自動で変えることができるようになる。
結構使うことになるので備忘録として。結論
Text
をFittedBox
配下に置く。sample.dartFittedBox( fit: BoxFit.fitWidth, child: Text( "あああああああああああああああああああああああテトラポット", style: TextStyle(fontSize: 32), ), ),FittedBoxがなかったら
折り返したり、画面からはみ出たりして、画面がぶっ壊れてしまう。
sample.dartText( "あああああああああああああああああああああああテトラポット", style: TextStyle(fontSize: 32), ),なので、この方法を覚えておくといいかも。
Swiftだとどうやって書くんだっけ?
interface builderでいつも設定してたから覚えてないや…
復習しないとな…
- 投稿日:2020-08-18T23:13:26+09:00
Xcode11でObjective-CをMRCで動かす
環境
- macOS Big Sur 11.0 Beta
- Xcode 11.6
Objective-C言語でMRC有効方法
Xcode11ではARCがデフォルト有効なため、
PROJECT
->Build Settings
からObjective-C Automatic Reference Counting
の値をNO
に変更する
main()を以下のように変更するとMRC環境で実行が可能となる。
int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); return UIApplicationMain(argc, argv, nil, appDelegateClassName); } }
- 投稿日:2020-08-18T21:54:21+09:00
Unable to process Runner.app.dSYM at path /Users/...省略.../Runner.app.dSYMが発生した場合の対処
Firebase Crashlyticsを導入してみたけど、うまく取得できないなーと思っていたら、エラーはいていた。
対応
この手順を実施したらエラー無くなりました。
- Xcode でプロジェクトを開きます。
- Xcode ナビゲータでプロジェクトを選択します。
- [Build Settings] タブを開きます。
- タブの上部にある [All] をクリックします。
- 「debug information format」を検索します。
- [Debug Information Format] を [DWARF with dSYM File] に設定します。
上記の手順が完了したら、アプリを再度ビルドして、Crashlytics が dSYM を検出できることを Firebase コンソールで確認します。
以上
- 投稿日:2020-08-18T19:37:44+09:00
Swift Package Manager とは
概要
Swift Package Manager(以降: SwiftPM) は、Swift コードの配布を管理するためのツールです。また、Swift のビルドシステムと統合されていることにより、依存関係のダウンロード・コンパイルなどが最適化されバイナリ、リソースがプロジェクトで使いやすい形になります。 SwiftPM は既存で存在する依存管理ツールの CocoaPods や Carthage とは違い Apple が OSS とし提供しているツールで、Xcode 9以降(Swift 3.0以降)に含まれています。
ライブラリを導入してみる
SwiftPM はXcode10 以前は
Package.swift
などに依存先を記述して、コマンドラインからのみビルド・実行しかできませんでした。そのため、サーバサイドや CUI アプリケーションなどでしか利用シーンがありませんでした。しかし、Xcode 11 以降からは.xcodeproj
に直接依存先の情報などを記述して導入できるようになったので、iOS アプリケーションでも SwiftPM が利用できるようになりました。つまり、CLI としてライブラリを利用する(SwiftFormatなど) + CLI ライブラリ、サーバサイドアプリなどを作成する以外は基本的には、Package.swift
を編集する必要はないということです。Lottie をインストール
File
>Swift Packages
>Add Package Dependency..
を開きます。Choose Package Repository 画面がでたら lottie-ios のリポジトリ URL を入力して次に進みます。
次にバイナリを指定する必要があるので、
version
・Branch
・Commit
で任意のものを選択して次に進みます。リポジトリの Fetch が終わると、ライブラリの Target 選択画面が開くので任意の Target を選択して次に進みます。
すると下記のようにインストールが確認できるとライブラリが利用可能な状態になります。
参考
- 投稿日:2020-08-18T18:57:09+09:00
web側がユニバーサルリンクの対応するときの備忘録
ユニバーサルリンクの対応【web】
ユニバーサルリンクの設定についての備忘録
とあるプロジェクトで苦労したユニバーサルリンクについての備忘録を書いていきます。順を追って困った時何をしたか、仕様について書いていこうと思います。
ユニバーサルリンク(Universal Links)とは
ios9からこの仕組みが利用できるようになりました。
webサイトをクリックしたときにiOSアプリを起動するための手法のことです。ディープリンクの一種です。もともとのお客様の求めている仕様はというと
「アプリを持っているときはアプリへ、持ってないときはAppStoreに飛んでほしい」 というものでした。
URLスキームであれば昔対応したことがあったのですが、ユニバーサルリンクは初めてでした。URLスキームと大して仕様かわらないっしょ!なんて軽い気持ちでいたがそんなことはなく、かなり大変でした。実装するにあたって必要な作業
ここからはios、web、サーバーで協力が必要だった必要作業です
apple-app-site-associationのファイル設定
ios側にユニバーサルリンクの設定が有効になるようにドメイン設定はしてもらうことはもちろんのこと(ios側の話は割愛です)、web側でも有効になるようにapple-app-site-associationというファイルを用意する必要がありました。
このファイルの注意点として、
* ファイル名をapple-app-site-associationから変えてはいけない
* ファイルはhttps://example.com/.well-known/apple-app-site-association または https://example.com/apple-app-site-association のようにルートディレクトリ、.well-knownサブディレクトリに配置します。
* このファイルは必ずHTTPSのwebサーバーに設置する自分が担当したものでは.well-knownサブディレクトリに配置しています。
web側では開発環境ごとにapple-app-site-associationの中身が違っていたので、ファイル名についてはapple-app-site-association-環境 という風にしておいて、ビルド時に「‐環境」の部分を外して使ってました。team idとbundle identifierをiosチームからもらうようにしましょう。
apple-app-site-associationで設定されるappIDは
team id+bundle identifierとなります。{ "applinks": { "apps": [], "details": [ { "appID": "team id+bundle identifier", "paths": [ "NOT /webview/passwordreset", "*" ] } ] } }同ドメインではweb⇒アプリに遷移しない問題があった
https://example.com/webview/authentication を閲覧中で、このドメインから https://example.com/ に遷移する際、アプリを持っていたらアプリに飛ぶ
という挙動にしたい場合、https://example.com/ が同ドメインだとアプリに遷移しません。(アプリに遷移するボタン等があった場合、気を付ける必要があった)https://example.test.jp/ ⇒ 遷移 ⇒ https://example.com/
という風にドメインを変える工夫が必要です。自分が担当したところでは、
https://website.jp/ こちらのドメインでユニバーサルリンクを設置していたため、
https://uni.website.jp/ (web or アプリ遷移用ボタン設置) ⇒ https://website.jp/ とすることでアプリを持っていたらアプリに遷移するということを実現できました。公式も凄く小さく注意書きしてあります。見逃します。。
App Search Programming Guide: Support Universal Linksまずこちらで準備は完了です。
ユニバーサルリンクが起動する条件
ユーザーアクションによる(URLをクリックする)遷移であること
- JSでのリダイレクトでは発動しません
- URLを直接検索バーに入力して遷移しても起動しません
ドメインが変わること
webに遷移した時でもサイト上部にユニバーサルリンクが設置していること(一度ユニバーサルリンクをクリックしてかつアプリを持っていれば以降アプリを開くようになる)
ユニバーサルリンクが起動しなくなってしまう条件
- apple-app-site-associationを設置しているサイトのURLを長押しすること
- これをするとアプリをもっていようがwebに遷移されます
- httpsではない
- ブラウザがSafariではない
- アプリで特殊なハンドリングをされてないこと(パラメータ等を受け取って指定したアプリのページを開くようにしましょう)
ちなみに
最初にお客様が言っていたアプリを持ってない場合にAppStoreに遷移させたいという内容はユニバーサルリンクの観点だけで書くと不可能でしたので、仕様にしてもらいました。
Apple Developer Documentation
アプリを持ってない場合はwebに飛ぶ仕様です。
自分の担当したところではアプリに遷移しない場合setTimeoutでappStoreに飛ぶように実装してましたが、ユニバーサルリンクの仕様とバッティングしてうまくいかず、webに飛んだりAppStoreに飛んだりしまいました。ユニバーサルリンクではwebに飛ぶのが仕様なのでAppStoreに飛ばす際はOneLink™ 概要 – ヘルプセンターなどの計測リンクを使ったり、webページに飛んでしまった時にAppStoreにリダイレクトするなりをしたほうがよさそうです。今回は部分的にoneLinkでアプリの有無判定を行っています。参考リンク
apple-app-site-associationのpaths(遷移対象パス)の作り方(UniversalLinks対応) - Qiita
UniversalLinksでのお問い合わせ,運用事例 - Qiita
[Swift] 同一ドメインでのユニバーサルリンク(Universal Links)は動作しないので注意 - Qiita
iOSアプリのユニバーサルリンクの仕組み - lasciva blogios13になったことで変わった仕様もあるようです。
UniversalLinks(ユニバーサルリンク)「apple-app-site-association」の書き方 パラメーター対応(iOS13以降) - Qiita
- 投稿日:2020-08-18T18:33:21+09:00
MarketingCloudSDK In-App Messaging概要
In-App Messagingとは
ユーザがアプリのプッシュ通知を許可していなくてもユーザにメッセージを配信できるアプリ内メッセージのことです。
アプリがフォアグラウンドの状態になる度にアプリ内メッセージが読み込まれ、SMCで作成したメッセージが、アプリのビュースタックの最上部に表示されます。(閉じたメッセージは再度表示されません)メッセージのテンプレートは以下3つがあります
・フルページ
レイアウトが画面全体に表示される
・バナー
画面最下部または最上部に表示される
・モーダル
画面の一部に全面表示される上記3つともSMCのContent Builderで設定可能です。
また色や、画像の配置、フォントサイズなどカスタマイズ可能になっています。
※文字のフォントに関してはデバイスのシステムフォントを使用しているため、アプリ側のフォント上書きの実装が必要です。通知するにはJourneyBuilderからアプリ内メッセージの送信が必要です。
実装
以下、公式ドキュメントから参照となります。
1.アプリ内メッセージング機能をViewControllerにデリゲートするために
sfmc_setEventDelegate
メソッドを使用を使用するViewController.swift// クラス宣言のMarketingCloudSDKEventDelegateプロトコルに従います。 class MyViewController: UIViewController, MarketingCloudSDKEventDelegate ... // 実装のどこかで、アプリ内メッセージイベントのためのSDKのデリゲートとしてクラスを設定します。 // 可能であれば、クラスの初期化の早い段階で行う必要があります。 MarketingCloudSDK.sharedInstance().sfmc_setEventDelegate(self)2.ビューの表示や削除に対応するデリゲートメソッドを追加する
ViewController.swiftfunc sfmc_didShow(inAppMessage message: [AnyHashable : Any]) { // message shown } func sfmc_didClose(inAppMessage message: [AnyHashable : Any]) { // message closed }3.アプリ内メッセージの表示を遅延させたり、防止したりすることができる。例えば、ロード中、インストラクション中、サインインフロー中などにアプリ内メッセージが表示されないようにする。メッセージの表示を防ぐには、
shouldShowInAppMessage
メソッドをfalseを返すようにする。ViewController.swiftfunc sfmc_shouldShow(inAppMessage message: [AnyHashable : Any]) -> Bool { // メッセージを表示するタイミングなどのロジックを書く if (self.messageCanBeShown) { return true } else { // メッセージデータを取得するできる self.showMessageId = self.sdk.sfmc_messageId(forMessage: message) } return false }4.メッセージデータを取得(
sfmc_messageIdForMessage
)して、後から表示(sfmc_showInAppMessage
)することが可能ViewController.swiftif (self.showMessageId != nil) { MarketingCloudSDK.sharedInstance().sfmc_show(inAppMessage: self.showMessageId) }5.アプリ内メッセージのフォントを変更するなら以下のデリゲートメソッドを使用し、デバイスにインストールされているフォントまたはアプリのカスタムフォントの有効なフォント名をSDKに渡す。
ViewController.swiftMarketingCloudSDK.sharedInstance().sfmc_set(inAppMessageFontName: "Zapfino")参考リンク(公式)
- 投稿日:2020-08-18T17:13:36+09:00
UITableView.tableHeaderViewにNibファイルから設定する場合
UITableView.tableHeaderViewにNibから生成されたViewを設定した時に、高さが0になってしまう現象に遭遇しました。
(何度かはまっている)探してみた
UITableView tableHeaderView auto layout
でググってみても解決策が出てきませんでした。以下は試してみましたができなかったものです。
- https://qiita.com/AkkeyLab/items/9864c741b9adb633b321
- https://bugsdb.com/_en/debug/094735585326ee5e720638561d7e4ea6解決方法
- Nib側のautoresizingで上下矢印の
Flexible Height
に印がついているので、外します。(上下矢印が消える)- 中身について上下左右にAutoLayoutで制限をつけます
- UITableView.tableHeaderViewにNibから生成したViewを入れます
- AutoLayoutエラーが起きていないことを確認します
考察
- NibからUITableView.tableHeaderViewに設定すると、AutoLayoutではなくAutoResizingMaskで設定される
- translatesAutoresizingMaskIntoConstraintsの設定と思われる
- 投稿日:2020-08-18T16:01:52+09:00
[iOS]Push通知を実装するにあたり、つまずいた点
前書き
iOSでPush通知を実装しようと奮闘していましたが、
プログラミング初心者の私がつまずいた点をいくつか紹介しますサーバーはニフクラを使用しました
以下の記事を参考に作成しました
Swiftでプッシュ通知を送ろう!
NIFCLOUDのgithubつまずいた点
1,証明書を何度も作り直してしまった
証明書を何度も作り直すことにより、PCが混乱してしまうことがあるので
キーチェーンアクセスから必要のない証明書を削除しましょう2,APNs用証明書(.p12)の作り方がわからなかった
以下参照
iosでプッシュ通知の証明書を.p12形式でexportしようとしたら.p12形式が選択できない問題について3,AppleDeveloperのProfileがinvalid(無効)になっていた
これはどうやって解決したのか詳しくは覚えていないが、すでにあるProvisioningProfileを削除した後、再度Profileを作成しProvisioningProfileをダウンロード
これは関係あるかわからないがProfileを作成する際のCertificates(証明書)を全てに選択した
4,Capabilityのつけ忘れ
Push Notificationsをつけ忘れるとAppDelegate内の
didRegisterForRemoteNotificationsWithDeviceToken deviceToken
が呼び出されずに端末情報を登録できないので注意
5,ドキュメントを読まなかった
きちんと書いてある。読んでクローンや!
NIFCLOUDのgithub証明書についてはこの記事から
プッシュ通知に必要な証明書の作り方20206,UIApplication.registerForRemoteNotifications() must be used from main thread onlyのエラーが出る
DispatchQueue.main.async(execute: { UIApplication.shared.registerForRemoteNotifications() })これで解決
7,Build SettingsのSigningの設定
8,BundleIdentifierについて
AppleDeveloperで作成したbundleIdentifier(ここではAとする)を作成後、
違うアプリのbundleIdentifierをAと書き換えることは可能9,なぜAppleDeveloperでデバイスの登録が必要なのか
未公開のアプリがセキュリティーに関係する機能を使用する際は、登録したデバイスのみであるため。
アプリが公開されればデバイス登録は必要ない。10,コード
最後にコードを載せておく
githubにコードは上がっているため必要ないと思うが一応AppDelegate.swiftimport UIKit import UserNotifications import NCMB @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? //********** APIキーの設定 ********** let applicationkey = "あなたのキー" let clientkey = "あなたのキー" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { //********** SDKの初期化 ********** NCMB.initialize(applicationKey: applicationkey, clientKey: clientkey) let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound]) {granted, error in if error != nil { // エラー時の処理 return } if granted { // デバイストークンの要求 DispatchQueue.main.async(execute: { UIApplication.shared.registerForRemoteNotifications() }) // UIApplication.shared.registerForRemoteNotifications() } } return true } // デバイストークンが取得されたら呼び出されるメソッド func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // 端末情報を扱うNCMBInstallationのインスタンスを作成 let installation : NCMBInstallation = NCMBInstallation.currentInstallation // デバイストークンの設定 installation.setDeviceTokenFromData(data: deviceToken) // 端末情報をデータストアに登録 installation.saveInBackground {result in switch result { case .success: // 端末情報の登録に成功した時の処理 break case let .failure(error): // 端末情報の登録に失敗した時の処理 print(error) break } } } func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } }最後に
一度成功すれば、なんて事のない実装だが、エラーが起きた際のデバックが大変だった。
また、証明書あたりを詳しく理解する必要がある。
- 投稿日:2020-08-18T00:12:12+09:00
iOS ReplayKitを調べてみた
iOS ReplayKitを調べた内容を纏めてみました。
環境
- macOS Big Sur 11.0 Beta
- Xcode 11.6
- 実機 iOS 13.6
機能概要
ReplayKitの主な流れ
- ReplayKitはiOS9から追加
- iOS11からアーキテクチャが変更され、RepalyKit2に
- iOS11からコントロールセンターの画面収録機能が追加
ReplayKitには、主に2つの機能がある
1. 画面録画
1.1. Screen Recording(iOS9〜)
1.2. In-App Screen Capture(iOS11〜)
2. Live Broadcasting
2.1. In-App Broadcast(iOS10〜)
- 実行中アプリの映像と音声を配信
2.2. Broadcast Pairing(iOS11〜)
2.3. iOS System Broadcast(iOS11〜)
2.4. System Broadcast Picker(iOS12〜)
実装方法
Screen Recording
RPScreenRecorder
とRPPreviewViewController
を使用するネットに情報が多いので割愛
In-App Screen Capture
RPScreenRecorder
を使用するネットに情報が多いので割愛
In-App Broadcast
RPBroadcastActivityViewController
を使用するいまいち使い方はよくわからず
Broadcast Pairing
RPBroadcastActivityViewController
を使用するpreferredExtension に
Broadcast Setup UI Extension
のBundle IDを設定するとリストにSetup UIが表示されるが、いまいち使い方はよくわからずiOS System Broadcast
- File → new → Target から
Broadcast Upload Extension
を追加するコントロールセンター → 画面収録 を長押しすると一覧に追加したExtensionが表示され、ブロードキャストを開始ができる
ブロードキャストを開始すると
Broadcasst Upload Extension
の processSampleBuffer() で Video / Audio Sample を受信するSystem Broadcast Picker
- 自アプリから画面収録画面を表示するには
RPSystemBroadcastPickerView
を使用するlet broadcastPicker = RPSystemBroadcastPickerView(frame: CGRect(x: 10, y: 50, width: 100, height: 100)) // 追加したBroadcast Upload ExtensionのBundleIDを設定 broadcastPicker.preferredExtension = kUploadExtension // マイクのオン/オフ表示を設定 broadcastPicker.showsMicrophoneButton = true // デフォルト表示の二重丸アイコンを文字列に変更 for subview in broadcastPicker.subviews { let b = subview as! UIButton b.setImage(nil, for: .normal) b.setTitle("画面共有", for: .normal) b.setTitleColor(.black, for: .normal) } self.view.addSubview(broadcastPicker)対応ソース
https://github.com/shima-0215/ReplayKitSample
ReplayKit対応アプリの動作
Zoom
ZoomはReplayKitを使用し、画面の共有をおこなう機能を有している
- コントロールセンターの画面収録を長押しし、
zoom
を選択しブロードキャストを開始する
をタップするとiOS画面を共有できる(iOS11〜)ミーティング中画面の
共有
から画面
をタップすると画面収録画面が表示され、zoom
を選択しブロードキャストを開始する
をタップするとiOS画面を共有できる(iOS12〜)
- iOS11の場合、
共有
から画面
をタップすると、コントロールセンターからのセットアップ方法の画面が表示されるhttps://support.zoom.us/hc/ja/articles/115005890803-iOS-Screen-Sharing
ミーティング中以外にコントロールセンターからブロードキャストを開始するとエラーとなる
画面の共有中に、他の端末でブロードキャストを開始されると、共有中の端末はエラーとなり終了する(後勝ちで動作)
その他
- Skype, Teams, LINE, LINE LIVE も対応してそうだが、動作は未確認。
今後、調べたいこと
アプリ本体側で iOS System Broadcast の開始と終了の通知を受け取りたい
Broadcast Upload Extension からアプリ本体側へ録画データを渡したい
- App Groups(UserDefaults)経由で渡せばよい?
- https://github.com/webex/spark-ios-sdk/wiki/Implementation-broadcast-upload-extension
参考サイト