20200818のiOSに関する記事は10件です。

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("失敗")
            }
        }
    }
}

少しでも参考になりましたら幸いです?

参考

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

【Flutter】画面サイズに応じて文字サイズを自動で適用する方法

この記事を読んで習得できること

画面サイズに応じて文字サイズを自動で変えることができるようになる。
結構使うことになるので備忘録として。

結論

TextFittedBox配下に置く。

sample.dart
FittedBox(
  fit: BoxFit.fitWidth,
  child: Text(
    "あああああああああああああああああああああああテトラポット",
    style: TextStyle(fontSize: 32),
  ),
),

スクリーンショット 2020-08-18 23.42.39.png

FittedBoxがなかったら

折り返したり、画面からはみ出たりして、画面がぶっ壊れてしまう。

sample.dart
Text(
  "あああああああああああああああああああああああテトラポット",
  style: TextStyle(fontSize: 32),
),

スクリーンショット 2020-08-18 23.41.56.png

なので、この方法を覚えておくといいかも。

Swiftだとどうやって書くんだっけ?
interface builderでいつも設定してたから覚えてないや…
復習しないとな…

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

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 に変更する
    スクリーンショット 2020-08-18 23.02.36.png

  • 上記だけだとXcode11では、main.mのmain()でEXC_BAD_ACCESSが発生してしまう。
    スクリーンショット 2020-08-18 23.03.31.png

  • 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);
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unable to process Runner.app.dSYM at path /Users/...省略.../Runner.app.dSYMが発生した場合の対処

Firebase Crashlyticsを導入してみたけど、うまく取得できないなーと思っていたら、エラーはいていた。

対応
この手順を実施したらエラー無くなりました。

  1. Xcode でプロジェクトを開きます。
  2. Xcode ナビゲータでプロジェクトを選択します。
  3. [Build Settings] タブを開きます。
  4. タブの上部にある [All] をクリックします。
  5. 「debug information format」を検索します。
  6. [Debug Information Format] を [DWARF with dSYM File] に設定します。

上記の手順が完了したら、アプリを再度ビルドして、Crashlytics が dSYM を検出できることを Firebase コンソールで確認します。

以上

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

Swift Package Manager とは

概要

Swift Package Manager(以降: SwiftPM) は、Swift コードの配布を管理するためのツールです。また、Swift のビルドシステムと統合されていることにより、依存関係のダウンロード・コンパイルなどが最適化されバイナリ、リソースがプロジェクトで使いやすい形になります。 SwiftPM は既存で存在する依存管理ツールの CocoaPods や Carthage とは違い Apple が OSS とし提供しているツールで、Xcode 9以降(Swift 3.0以降)に含まれています。

image.png

ライブラリを導入してみる

SwiftPM はXcode10 以前は Package.swift などに依存先を記述して、コマンドラインからのみビルド・実行しかできませんでした。そのため、サーバサイドや CUI アプリケーションなどでしか利用シーンがありませんでした。しかし、Xcode 11 以降からは .xcodeproj に直接依存先の情報などを記述して導入できるようになったので、iOS アプリケーションでも SwiftPM が利用できるようになりました。つまり、CLI としてライブラリを利用する(SwiftFormatなど) + CLI ライブラリ、サーバサイドアプリなどを作成する以外は基本的には、Package.swift を編集する必要はないということです。

Lottie をインストール

File > Swift Packages > Add Package Dependency.. を開きます。

image.png

Choose Package Repository 画面がでたら lottie-ios のリポジトリ URL を入力して次に進みます。

image.png

次にバイナリを指定する必要があるので、versionBranchCommit で任意のものを選択して次に進みます。

image.png

リポジトリの Fetch が終わると、ライブラリの Target 選択画面が開くので任意の Target を選択して次に進みます。

image.png

すると下記のようにインストールが確認できるとライブラリが利用可能な状態になります。

image.png

参考

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

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 blog

ios13になったことで変わった仕様もあるようです。
UniversalLinks(ユニバーサルリンク)「apple-app-site-association」の書き方 パラメーター対応(iOS13以降) - Qiita

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

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.swift
func sfmc_didShow(inAppMessage message: [AnyHashable : Any]) {
    // message shown
}

func sfmc_didClose(inAppMessage message: [AnyHashable : Any]) {
    // message closed
}

3.アプリ内メッセージの表示を遅延させたり、防止したりすることができる。例えば、ロード中、インストラクション中、サインインフロー中などにアプリ内メッセージが表示されないようにする。メッセージの表示を防ぐには、shouldShowInAppMessageメソッドをfalseを返すようにする。

ViewController.swift
func 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.swift
if (self.showMessageId != nil) {
    MarketingCloudSDK.sharedInstance().sfmc_show(inAppMessage: self.showMessageId)
}

5.アプリ内メッセージのフォントを変更するなら以下のデリゲートメソッドを使用し、デバイスにインストールされているフォントまたはアプリのカスタムフォントの有効なフォント名をSDKに渡す。

ViewController.swift
MarketingCloudSDK.sharedInstance().sfmc_set(inAppMessageFontName: "Zapfino")

参考リンク(公式)

https://salesforce-marketingcloud.github.io/MarketingCloudSDK-iOS/in-app-message/in-app-messaging.html

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

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エラーが起きていないことを確認します

スクリーンショット 2020-08-18 14.49.08.png

考察

  • NibからUITableView.tableHeaderViewに設定すると、AutoLayoutではなくAutoResizingMaskで設定される
    • translatesAutoresizingMaskIntoConstraintsの設定と思われる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS]Push通知を実装するにあたり、つまずいた点

前書き

iOSでPush通知を実装しようと奮闘していましたが、
プログラミング初心者の私がつまずいた点をいくつか紹介します

サーバーはニフクラを使用しました

以下の記事を参考に作成しました
Swiftでプッシュ通知を送ろう!
NIFCLOUDのgithub

つまずいた点

1,証明書を何度も作り直してしまった

証明書を何度も作り直すことにより、PCが混乱してしまうことがあるので
キーチェーンアクセスから必要のない証明書を削除しましょう

2,APNs用証明書(.p12)の作り方がわからなかった

以下参照
iosでプッシュ通知の証明書を.p12形式でexportしようとしたら.p12形式が選択できない問題について

3,AppleDeveloperのProfileがinvalid(無効)になっていた

これはどうやって解決したのか詳しくは覚えていないが、すでにあるProvisioningProfileを削除した後、再度Profileを作成しProvisioningProfileをダウンロード
スクリーンショット 2020-08-18 15.20.17.png

その後Xcodeで正しいProfileが入っているか確認
スクリーンショット 2020-08-18 15.23.05.png

これは関係あるかわからないがProfileを作成する際のCertificates(証明書)を全てに選択した

4,Capabilityのつけ忘れ

Push Notificationsをつけ忘れるとAppDelegate内の
didRegisterForRemoteNotificationsWithDeviceToken deviceToken
が呼び出されずに端末情報を登録できないので注意
スクリーンショット 2020-08-18 15.26.59.png

5,ドキュメントを読まなかった

きちんと書いてある。読んでクローンや!
NIFCLOUDのgithub

証明書についてはこの記事から
プッシュ通知に必要な証明書の作り方2020

6,UIApplication.registerForRemoteNotifications() must be used from main thread onlyのエラーが出る

訳すと「メインスレッドから使用しろやボケ」とのこと
スクリーンショット 2020-08-18 15.37.23.png

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

これで解決

7,Build SettingsのSigningの設定

スクリーンショット 2020-08-18 15.42.11.png

8,BundleIdentifierについて

AppleDeveloperで作成したbundleIdentifier(ここではAとする)を作成後、
違うアプリのbundleIdentifierをAと書き換えることは可能

9,なぜAppleDeveloperでデバイスの登録が必要なのか

未公開のアプリがセキュリティーに関係する機能を使用する際は、登録したデバイスのみであるため。
アプリが公開されればデバイス登録は必要ない。

10,コード

最後にコードを載せておく
githubにコードは上がっているため必要ないと思うが一応

AppDelegate.swift
import 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:.
    }
}

最後に

一度成功すれば、なんて事のない実装だが、エラーが起きた際のデバックが大変だった。
また、証明書あたりを詳しく理解する必要がある。

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

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〜)

  • 実行中のアプリを録画し、プレビューコントローラーから写真や共有で保存 スクリーンショット 2020-08-17 22.45.19.png

1.2. In-App Screen Capture(iOS11〜)

  • 実行中のアプリを録画した映像と音声を取得 スクリーンショット 2020-08-17 22.45.36.png

2. Live Broadcasting

2.1. In-App Broadcast(iOS10〜)

  • 実行中アプリの映像と音声を配信

2.2. Broadcast Pairing(iOS11〜)

2.3. iOS System Broadcast(iOS11〜)

  • 実行中アプリ以外のiOS画面も配信可能に
  • コントロールセンターの画面収録からブロードキャスト配信を行う設定が追加 _2020-08-17_22.53.06.png

2.4. System Broadcast Picker(iOS12〜)

  • 自アプリから画面収録画面を表示 スクリーンショット 2020-08-17 22.54.59.png

実装方法

Screen Recording

RPScreenRecorderRPPreviewViewController を使用する

ネットに情報が多いので割愛

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 も対応してそうだが、動作は未確認。

今後、調べたいこと

参考サイト

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