20200604のiOSに関する記事は9件です。

Application Development Guide for Android and iOS

The complete guide on how to create a mobile application, from defining the project goal and technology stack to testing and deployment. Check it out!

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

How to attract more customers for your mobile app

How to promote a smartphone app, attract users, and earn an income? Check out the best mobile application marketing tools and strategies.

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

Top 15 Apps Like Instagram for IOS and Android you need to know about

Though Instagram continues to be the leading photo-sharing app, it has strong competitors. Check out 15 Instagram-like apps to see their success formula.

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

イテレータをSwift5で実装する

※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。

The Iterator(イテレータ)

0. イテレータの意義

複数の要素の集合について、全要素に対し順に同様の処理を行うデザインパターンをイテレータという。

Swiftではfor-inループなどで使うことが出来る。

Array型, Set型, Dictionary型などあらかじめ用意されている型はもちろんだが、カスタムタイプもSequenceプロトコルに対応させることでfor-inループで回すことができる。

今回はキューを実装した後、それをSequenceプロトコルに対応させfor-inループで回せるようにする。

1. カスタムタイプ(キュー)の実装

キューとは、複数の要素を扱う型の一種でFirst-in Last-out(最初に追加した要素が最後に取り出される)が特徴の物を言う。

SwiftではUITableViewやUICollectionViewを使うときに、構成要素のセルをキューで管理するようになっているためよく見かけるかもしれない。

まず、自前でキューを実装する。キューの要素(ノード)を定義する必要がある。

Queue.swift
private final class Node<T> {

    //ジェネリクスTを用いて任意の肩を格納できる
    var key: T?    

    //~中略~

    init(_ value: T? = nil) {
        self.key = value
    }
}

キュー内では、複数のノードが連続して連なることになる。そのため、次のノードを参照する変数nextを用意する。

    var next: Node?

次にキュー本体を実装する。キューの先頭と最後のノードを参照する変数headtailを定義する。キューに一個も要素がない場合は先頭も最後もないことから、nilを許容するoptional型とする。

Queue.swift
public final class Queue<T> {
    fileprivate var head: Node<T>?
    private var tail: Node<T>?

    public init(){}
//後略

新しい要素をキューの一番最後に追加するメソッドenqueue(_:)を定義する。キューの中に一つも要素がなく、最初の要素を追加する時は少し処理が違うため注意する。

Queue.swift
    public func enqueue(_ value: T) {
        //渡された値を元にノード(要素)を生成
        let newNode = Node<T>(value)

        guard let _ = self.head else {
            //キューに要素が一つもない場合(一つ目の要素を追加する場合)の処理
            self.head = newNode
            self.tail = self.head
            return
        }

        //キューにすでに要素がある場合(二つ目以降の要素を追加する場合)の処理
        self.tail?.next = newNode
        self.tail = newNode
    }

デキュー(先頭の要素をキューの中から取り出す)関数を定義する。

Queue.swift
    public func dequeue() -> T? {
        //先頭にものがない(キューに要素が一つもない)場合はnilを返す
        guard let headItem = self.head?.key else {
            return nil
        }

        if let nextNode = self.head?.next {
            //二つ目の要素がある場合は、これを新しい先頭の要素とする
            self.head? = nextNode
        } else {
            //二つ目の要素がない場合は、デキューにより一つも
            //要素がなくなることに注意
            self.head = nil
            self.tail = nil
        }

        //先頭の要素を返す
        return headItem
    }

その他標準的な機能を実装する。
empty() -> Bool: キューが空(要素数がゼロ)かどうかを判定する関数
peak() -> T: キューの先頭の要素を返す(デキューはしない)

Queue.swift
    public func isEmpty() -> Bool {
        return head == nil
    }

    public func peek() -> T? {
        return self.head?.key
    }

しかし、これだけではキューをfor-inループで回すことはできない。下記エラー文にあるように、キューをSequenceプロトコルに準拠させる必要がある。

Queue.playground
var queue = Queue<Int>()
queue.enqueue(1)
queue.enqueue(2)

for item in queue {//error: type 'Queue<Int>' does not conform to protocol 'Sequence'
    print(item)
}

2. Sequenceプロトコルへの準拠

Sequenceプロトコルに準拠させるにはQueueIteratorという型をタイプエイリアスとして定義し、関数makeIterator()でその型を返す必要がある。

Queue.swift
extension Queue: Sequence {

    public typealias Iterator = QueueIterator<T>

    public func makeIterator() -> Queue<T>.Iterator {
        return QueueIterator(self)
    }
}

QueueIteratorは、また別のプロトコルIteratorに準拠している必要がある。

重要なのはnext()関数である。

この関数は、for-inループで回した時に、ある要素の処理が終わった後の次の要素を返す関数となっている。

「次の要素」が何か? という事を定義するには現在の要素が何か?という点を管理する必要がある。そのためcurrentNodeという変数を用意し、next()関数が呼ばれたらキュー上の次の要素を返す実装とする。

Queue.swift
public struct QueueIterator<T>: IteratorProtocol {

    public typealias Element = T

    private let queue: Queue<T>
    private var currentNode: Node<T>?

    init(_ queue: Queue<T>) {
        self.queue = queue
        self.currentNode = queue.head
    }

    mutating public func next() -> QueueIterator<T>.Element? {
        guard let node = self.currentNode else {return nil}

        let nextKey = node.key
        self.currentNode = node.next
        return nextKey
    }
}

これでキューがSequenceプロトコルに準拠した。for-inループの中で使えるようになる。

Queue.playground
var queue = Queue<Int>()
queue.enqueue(1)
queue.enqueue(2)

for item in queue {
    print(item)//1 2
}

https://github.com/Satoru-PriChan/QueueIteratorDemo

参考文献: https://www.amazon.com/Design-Patterns-Swift-implement-Improve-ebook/dp/B07MDD3FQJ

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

UIImageからGIFを生成する

個人開発アプリTweetFly
複数枚の画像をGIFに変換する機能がありまして、ちょっと調べてまとめました。

下のGIFはString -> UIImage -> GIFに変換したサンプルです。
sample_typewritter.gif

import UIKit
import ImageIO
import MobileCoreServices

class GIFCreator {

    struct GIFFrame {
        var image: UIImage
        var duration: Float
    }

    static func create(with frames: [GIFFrame], resizeTo size: CGSize? = nil, filename: String = "temp.gif", completion: @escaping (URL?) -> Void) {
        DispatchQueue.global(qos: .userInteractive).async {

            // Resize images if needed
            let resizedFrames: [GIFFrame]
            if let size = size {
                let format = UIGraphicsImageRendererFormat()
                format.scale = 1
                resizedFrames = frames.map { f in
                    let resizedImage = UIGraphicsImageRenderer(size: size, format: format).image { _ in
                        f.image.draw(in: CGRect(origin: .zero, size: size))
                    }
                    return GIFFrame(image: resizedImage, duration: f.duration)
                }
            } else {
                resizedFrames = frames
            }

            let cacheUrl = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            let url = cacheUrl.appendingPathComponent(filename)
            try? FileManager.default.removeItem(at: url)
            let cfURL = url as CFURL

            if let destination = CGImageDestinationCreateWithURL(cfURL, kUTTypeGIF, resizedFrames.count, nil) {
                let fileProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]]
                CGImageDestinationSetProperties(destination, fileProperties as CFDictionary?)
                for frame in resizedFrames {
                    let gifProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: frame.duration]]
                    CGImageDestinationAddImage(destination, frame.image.cgImage!, gifProperties as CFDictionary?)
                }
                DispatchQueue.main.async {
                    CGImageDestinationFinalize(destination) ? completion(url) : completion(nil)
                }
            } else {
                DispatchQueue.main.async {
                    completion(nil)
                }
            }
        }
    }

    static func create(with images: [UIImage], perFrameDuration: Float = 0.1, resizeTo size: CGSize? = nil, filename: String = "temp.gif", completion: @escaping (URL?) -> Void) {
        GIFCreator.create(with: images.map { GIFFrame(image: $0, duration: perFrameDuration)}, resizeTo: size, filename: filename, completion: completion)
    }
}

使い方は
1、各画像の表示時間が固定の場合は[UIImage]GIFCreatorに渡せばOKです。

let images = ...
GIFCreator.create(with: images) { url in
    guard let url = url, let data = try? Data(contentsOf: url) else { return }
    let gif = UIImage(data: data)
}

2、各画像の表示時間が個別で設定したい場合

let gifFrames: [GIFCreator.GIFFrame] = [
            GIFCreator.GIFFrame(image: UIImage(), duration: 0.1),
            GIFCreator.GIFFrame(image: UIImage(), duration: 0.2),
            GIFCreator.GIFFrame(image: UIImage(), duration: 0.3)
        ]
GIFCreator.create(with: gifFrames) { url in
    guard let url = url, let data = try? Data(contentsOf: url) else { return }
    let gif = UIImage(data: data)
}

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

Unity IAPは公式APIドキュメントがないので課金処理は自前ネイティブ実装がいいかも

まとめ

  • https://docs.unity3d.com/Manual/UnityIAP.html
  • マニュアルはある。しかしAPIについての最新の説明は存在しないので自己責任でやるしかない。
  • 自分でネイティブ連携するのが最強

現状

-https://forum.unity.com/threads/where-has-unity-iap-scripting-reference-gone.660454/#post-4543948

Understood on the concern. As we are moving more towards Package Manager, we've had to adjust our documentation work flow, and it's still a work in progress. It is something we are actively working on, apologies on the inconvenience.
JeffDUnity3D, May 15, 2019
(今作業中です(2019/05/15))

つまり1年以上放置されてるわけで Unity 2018.3 以降は最新の情報を得る手段がありません。つまりLTS基準で考えると2018版から現在までドキュメントのないものを雰囲気で実装してるわけでAppleやらGoogleやらの方でPurchasing APIに変更やら機能追加やらが入ったときにちゃんと追従されるかも怪しい気がします。

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

iOSのダウングレード手順

iOSのダウングレード手順を説明します。

iTunesがなくなったので、Finderから行います。

iOS FW(ips)を以下からダウンロードします。ダウングレードできるバージョンは、Signed IPSWsのみ有効です。

https://ipsw.me/

次に、Finderを開いて、ダウングレードしたいデバイスを選択します。

次に、macの場合は、option(alt)を押しながら、winの場合はshiftを押しながらアップデートを確認ボタンをクリックします。

すると、ファイルを選択できるようになるので、先ほどダウンロードした.ipsを選択して、ダウングレードできます。

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

FirebaseCrashlyticsへのupload-symbols実行時にCould not fetch upload-symbolsのエラーが発生する

リリースしたアプリのクラッシュログ分析などでよく用いられるFirebaseCrashlytics。
FirebaseCrashlyticsを使用して、クラッシュログをもれなく収集するには、ライブラリ内のメソッドを使用するなどしてdsymファイルをFirebaseにアップロードする必要があるのですが、ここで表題のCould not fetch upload-symbolsのエラーが発生しました。同じエラーが発生した人の助けになればと思います。

結論

結論から記述すると、Firebase側のコンソール設定が完了していなかったことが原因でした。
ただ、権限の関係で本番環境のコンソールが確認できなかった、なぜかFabricのライブラリ内にあるコマンドではアップロードができてしまった、などの複数の要因から原因に気づくのにも時間がかかりました。
コンソール画面の閲覧権限がないなど、同じような事象に出くわす方もいるかと考え、今回の記事作成に至りました。

どんなことをしたか

FirebaseCrashlyticsについて

まずFirebaseCrashlyticsについて簡単に説明しておきます。
Firebaseが提供しているサービス。ライブラリの導入とアプリ起動時に数行の設定コードを記述することで、クラッシュログを収集しコンソール画面で管理ができます。

公式ドキュメントに従ってセットアップする必要があります。
(日本語記事もありますが、記述が古い箇所があるので英語ドキュメントを参照するのがよいと思います)
本記事はこのセットアップ過程の中でFirebaseCrashlyticsのdSYMファイルアップロードの工程についての記事となります。

CrashlyticsはもともとFabricによって提供されていたサービスで、FabricのFirebaseへの統合に伴い、CrashlyticsもFirebaseに移行されました。もともとFabricを使っていた場合、dSYMアップロードの際に若干方法が異なるので注意が必要です。

dSYMのアップロード方法

FirebaseCrashlyticsの公式ドキュメントに書いてある方法ですが、以下のコマンドをターミナルなどのコマンドラインから叩くことでアップロードが可能です。

/path/to/pods/directory/FirebaseCrashlytics/upload-symbols -gsp /path/to/GoogleService-Info.plist -p ios /path/to/dSYMs

GoogleService-Info.plist・dSYMファイルのパスを指定して、ライブラリに用意されているコマンドを叩きます。
アップロードに成功するとSuccessfully uploaded Crashlytics symbolsが表示されます。

Fabricを使っている場合には、この叩くべきコマンドが以下に変わります。

/path/to/pods/directory/Fabric/upload-symbols -a <FabricAPIKey> -p ios /path/to/dSYMs

Fabricの場合は、FabricのAPIKeyを指定してあげないといけないみたいです。

発生したエラーとその原因

FirebaseCrashlyticsライブラリのコマンドにて実行した際に、以下のエラーが発生しました。

Fetching upload-symbols settings...
error: Could not fetch upload-symbols settings: An unknown error occurred fetching settings.

調べてみると協業先のアカウントではFirebaseCrashlyticsの設定が行われていなかったことがわかりました。
これを設定後に再度試してみると、無事Successfully uploaded Crashlytics symbolsとなり、アプリリリース後も無事クラッシュログを収集できました。

本来、アカウント側の設定は一番最初に実施すべき工程として公式ドキュメントに記載されているので、完全なる凡ミスではあるのですが、開発用と本番用とでアカウントをわけていたり、コンソールが確認できないなどの事情があると、こう言ったケースは起こりうるのではないかなと思います。
当該のエラーが出た方がいたら、まずはアカウント側の設定を見直していただければよいかもしれません。

補足

冒頭で「なぜかFabricのライブラリ内にあるコマンドではアップロードができてしまった」というふうに書きました。
これはアップロードに失敗した時に、試しにFabricで用意された下記のコマンドを叩いてみるとなぜかSuccessしてしまう、という事象でした。

/path/to/pods/directory/Fabric/upload-symbols -gsp /path/to/GoogleService-Info.plist -p ios /path/to/dSYMs

その後Firebase側のコンソール設定が未完了ということが判明したのですが、なぜこのFabricのコマンドではSuccessしてしまうのかが謎のままでした。

しかしこの記事を書く過程でみつけた記事でこんな記述にあたりました。

Fabric時代のアプリも通常のコマンドでアップロードして「Successfully uploaded Crashlytics symbols」と表示されるのですが、Firebase上では追記したコマンドでやらないといつまでたっても認識されていないようでした。

この記事の通常のコマンドとは、gspオプションでGoogleService-Info.plistのパス指定しているコマンドを指しているのですが、こちらの指定方法だとSuccessと表示されるものの、Firebase側には反映されていないようです。
もしかするとこちらのコマンドを叩いていることで、見かけ上成功しているが実際はアップロードされていなかったのかもしれません。
Fabricの場合のコマンドをググると、2パターンの指定方法がひっかかってしまいますが、APIキーを指定するコマンドを使わなくてはならないようですので、その点も要注意です。

参考

Firebase公式 - Get deobfuscated crash reports with the Firebase Crashlytics SDK
Tomo.Log() - FirebaseでdSYMをアップロードする方法が変わってた

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

PlayFabでのプッシュ通知設定が難しすぎた

UnityのバックエンドをPlayFabに任せてみようと試みていたのですが、iOSのプッシュ通知設定が難しすぎたので備忘録です。

pemファイルのアップロードができない

PlayFabのプッシュ通知設定手順については、公式や紹介ブログがあるので詳細は割愛しますが、pemファイルをアップロードする段階で以下のようなエラーが発生してしまいます。

Invalid parameter: Reason: cannot be empty

スクリーンショット 2020-06-04 1.54.16.png

pemの発行には問題無さそうで、まるでエラーの検討がつきません。

解決策

ぐぐってみると以下のフォーラムを発見。
https://community.playfab.com/questions/36895/instruction-on-using-admin-api-to-set-up-ios-push.html

一言で言うと、「管理画面のUIはバグってて使い物にならないから、AdminAPIを使え」とのことでした。

そしてこのAdmin API、pemファイルの中身をいい感じに整形してリクエストパラメータに乗っける、という癖の強い仕様になっています。

リクエストパラメータの設定方法

以下フォーラム記事を翻訳したリクエストパラメータの設定手順です。

  1. テキストエディタを使ってPEMファイルを開きます。2つの文字列のセクションがあり、1つは証明書、もう1つは秘密鍵です。

  2. 証明書のテキストを別の空のテキストファイルにコピーします。また、"-----BEGIN CERTIFICATE-----"と"-----END CERTIFICATE-----"のマーカーも必ずコピーしてください。そして、テキストエディタのreplaceメソッドを使って、すべての改行を「\n」に正確に変換して、1行の文字列にしてください。これはどのPlayFab公式ドキュメントにも記載されていません(えっ)

  3. 秘密鍵部分も同様に繰り返します。鍵のテキストの改行をすべて変換します。

  4. PlayFab APIを呼び出すことができる環境(Postman、.NET Coreのコンソールアプリ、またはUnityのEditorスクリプト)で、SetupPushNotificationを呼び出します。

5 リクエストの "Key "プロパティに証明書の文字列を設定します。

6 Credentialプロパティに秘密鍵の文字列を設定します。

APIのインタフェースは公式を参照。
https://docs.microsoft.com/ja-jp/rest/api/playfab/admin/title-wide-data-management/setuppushnotification?view=playfab-rest

分からなさすぎる

この人はどうやってこの解決策にたどり着けたのか謎です。すごい。。。
おかげで無事にiOSのプッシュ通知設定ができました。

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