20210124のiOSに関する記事は8件です。

ちゃんと紐づけてるのにunrecognized selector sent to instanceエラーが出る時の対処方法

このエラーに数日悩まされたので備忘録として残します。

Bar Button Itemタップ時の処理を実装していたところ、
以前までは正常に実装できていたものが、「unrecognized selector sent to instance」のエラーで前に進めなくなった。

調べてみると、いくつか要因があるとのこと

  1. identity inspectorのClassの設定が間違っている
  2. Inherit Module From Targetにチェックが入っていない(自分はこれが原因だった)
  3. タップ時のアクションを示す関数にて、引数(例:_ sender: UIBarButtonItem)が設定されていない

(2が原因の場合、Inherit Module From Targetにチェックを入れるか、もしくは、Moduleに手打ちすればよいとのこと)

参考になればいいな

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

FlutterでiOSアプリの名前を日本語で設定する簡単な方法

ウェブで調べるとXcodeを使っての情報と日本語では簡単には設定できない旨の情報ばかりで、私のようにWindows→Codemagicで開発をしている物には参考情報が得られませんでした。
これを友人にぐちったところあっさり解決策が見つかったので、メモを兼ねて記載します。
プロジェクトフォルダ\ios\Runner\Info.plistを編集する点は同じです。


一番多く見つかる情報

先に注意を兼ねて半分間違いの情報です。

<key>CFBundleName</key>
<string>MyApp</string>

CFBundleNameのstringにアプリ名を入力します。
ただしこれでは日本語の名前は設定できません。

正解の情報

<key>CFBundleDisplayName</key>
<string>私のアプリ</string>

とします。
CFBundleDisplayNameはプロジェクト作成時には記載がありませんので追記する必要があります。

まとめ

Xcodeでローカライズが必要などの情報ばかりでしたが、こんな簡単に指定ができると知って拍子抜けでした。
今後クロスプラットフォーム開発が売りなので、Androidのアプリ名と合わせてプロジェクト作成のときなどに簡単に設定できるようにして欲しい物です。

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

あなたのiPhoneを一瞬でアイプラフォンにしちゃおう!

動機

アイカツプラネット!始まりましたね!
筐体ではアイカツオンパレード!以前と同様に2次元バーコードを読み込ませる方式ですが、
テレビシリーズではアイプラフォンと呼ばれる限りなくiPhoneに近いデバイスにセットしています。

このままでは筐体で遊ぶときに混乱してしまうので、iPhoneをアイプラフォンにしてしまいましょう。

用意するもの

iPhone (XS以降、iOS13.1)
NFCタグ(シールになってるやつがいいです)
マイページ登録済みのアイドルライセンス

オートメーションの設定

前項でほとんどの方がピンときたと思いますので、ここからはほぼ蛇足となる予感がしています(笑

ショートカットアプリで"個人用オートメーション"を作成します。
IMG_6231.png

オートメーションのきっかけとして"NFC"を選択し、用意したNFCタグをスキャンします。
IMG_6232.png

タグの名称はなんでもいいんですが、ここでは"アイドルライセンス"としておきます。

"次へ"をタップでオートメーションのアクション選択に移るので、Safariの"URLを開く"を選びます。
開くURLは https://mypage.aikatsu.com/digital-licences を指定します。
IMG_6245.png

"次へ"をタップして確認画面に移行し、実行の前に尋ねるをオフにして"完了"をタップして設定は完了です。
IMG_6242.png

やってみよう

Safariでマイページにログインしておきます。

登録したNFCタグをアイドルライセンス裏の上部に貼り付けます。
IMG_6244.png

アニメのようにiPhoneのうしろにアイドルライセンスをスライドするとマイページのデジタルライセンスが開きます。

実際の様子を動画でご覧ください。
これでなりたい私に、ミラーイン!

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

TCAでdelegateを処理するパターン

はじめに

この発表資料は、
iOSアプリ開発のためのFunctional Architecture情報共有会3の発表資料です。

内容として、TCAがdelegateを処理するパターンを整理し、Core DataのNSFetchedResultsControllerDelegateで自動で差分を検知してくれる仕組みをTCAでも使えるようにするという内容です。


導入

iOSDC2020 iOSアプリ開発のための"The Composable Architecture"がすごく良いので紹介したいで出た質問から

TCAではActionはViewStoreからしか発火できないので、ユーザ操作からしかActionを発火できないですよね?(だからAppDelegateのイベントとかからActionを呼び出せないのどうするんですか?)


ActionはViewStoreからしか呼び出せないわけじゃない

  • ActionはAction自体から呼び出せる
    • そもそもReducerはEffectを返しそこで別のActionにmapすることができる
  • 例えば何かを監視する際にdelegateの実行をトリガーにしてActionを呼び出せる

  1. アプリ起動でView表示
  2. SwiftUIならonAppear { viewStore.send( Action.A ) }
  3. Reducerの処理でDelegateを作成して実行しておく
  4. delegateの処理で実行されてそこからSubscriberがAction.Bを呼び出す

話すこと

ユーザアクションに縛られないActionの発行


参考

  • TCAが用意してくれてるライブラリ
    • LocatoionManager
    • MotionManager
  • TCAのサンプル
    • WebSocketClient
    • SpeechClient

ユーザアクションに縛られず、何かしらを監視しdelegateで処理を呼び出し、SubscriberでActionを発火させてる例がある


余談: よくわからないこと

  • ManagerとClientの名前の違いはどこから?
    • Managerが専用のActionとかErrorを持ってるわけじゃない
    • LocationManager
      • Action
      • Error
    • MotionManager
    • WebSocketClient
      • Action
      • Error
    • SpeechClient
      • Action

共通するパターン

  • 監視開始用クロージャを定義
  • staticな初期化でシングルトン的に作れるようにする(.live)
    • 監視開始用クロージャを上書きする
    • delegateを作成
    • delegateが実行するメソッドからActionを呼び出すようにする
  • Reducerからシングルトンを呼び出す
    • Actionを次のActionにmapできるようにする

実際のコードからこのパターンを理解する


LocationManager

  • ユーザの位置情報をリアルタイムに検知してActionを発火してその位置を教えてくれる
  • ReducerのActionとして位置情報を伝えてくれる

TCA_delegate.001.png


実際のコード


プロパティとしてクロージャを用意

上書き可能なロジックを用意しておく

  • 上書きされる前に実行すると未実装としてクラッシュ
    • これはテストコードでも任意の実装に置き換えも可能
public struct LocationManager {
  ...
  var create: (AnyHashable) -> Effect<Action, Never> = { _ in 
      // ActionはLocationManager.Actionを省略できている
      _unimplemented("create") 
  }
  ...
}

Managerをstaticに作成

  • シングルトン的にしておく
    • classじゃなくてstructでいい
    • 状態の変化を通知するオブジェクトは重複してはいけないし壊されてもいけない
public struct LocationManager {
    ...
}

extension LocationManager {
  public static let live: LocationManager = { () -> LocationManager in
    var manager = LocationManager()

    ... /* 次にここで managerのクロージャを上書きする処理を書く */

    return manager
  }
}

先程用意したcreateプロパティにクロージャを代入

  • やること
    • iOSのCLLocationManagerでロケーションを探せるように
      • delegateをセットしEffectのsubscriberを渡す
    • privateなグローバル変数dependenciesにidをキーにして保持
      • delegateやmanagerやsubscriberを保持
    • AnyCancellableで終了時/キャンセル時にクリーンアップ
extension LocationManager {
  public static let live: LocationManager = { () -> LocationManager in
    var manager = LocationManager()    
    ...
    // ここから追記した __________________
    manager.create = { id in
      Effect.run { subscriber in
        let manager = CLLocationManager()
        var delegate = LocationManagerDelegate(subscriber)
        manager.delegate = delegate

        dependencies[id] = Dependencies(
          delegate: delegate,
          manager: manager,
          subscriber: subscriber
        )

        return AnyCancellable {
          dependencies[id] = nil
        }
      }
    }
    // 追記ここまで 
    ...

    return manager
  }
}

CLLocationManagerDelegateとは
  • Locationが変わった際のprotocolを定義している
    • delegateで特定のメソッドが実行される
public protocol CLLocationManagerDelegate : NSObjectProtocol {

    @available(iOS 6.0, *)
    optional func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
    ...
}

CLLocationManagerDelegateのメソッドでSubscriberがイベントを発火させる

  • let subscriber: Effect<LocationManager.Action, Never>.Subscriberをもたせる
  • subscriber.sendでActionを呼び出す
private class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
  let subscriber: Effect<LocationManager.Action, Never>.Subscriber

  // エラー時
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    subscriber.send(.didFailWithError(LocationManager.Error(error)))
  }
  // location変更時
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    subscriber.send(.didUpdateLocations(locations.map(Location.init(rawValue:))))
  }
  ...
}

Reducerからシングルトンを呼び出す

  • .create(id: LocationManagerId())
    • 実行時のユニークなハッシュをIDに
    • mapで次のアクションAppAction.locationManagerに変換
  • combineでReducerを繋げる
public enum AppAction: Equatable {
  case onApper
  case locationManager(LocationManager.Action)
  ...
}
...
public let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
  switch action {
  ...
  case .onAppear:
    return environment.locationManager.create(id: LocationManagerId())
      .map(AppAction.locationManager) // combineしているlocationManagerReducerを呼び出す
  ...
  }
}
.combined(
  with:
    locationManagerReducer
    .pullback(state: \.self, action: /AppAction.locationManager, environment: { $0 })
)
.signpost()
.debug()

private let locationManagerReducer = Reducer<AppState, LocationManager.Action, AppEnvironment> {
  state, action, environment in
  ...
}

Core DataのNSFetchedResultsControllerDelegateでDB監視を利用する

  • 監視開始用クロージャを定義
  • staticな初期化でシングルトン的に作れるようにする(.live)
    • 監視開始用クロージャを上書きする
    • delegateを作成
    • delegateが実行するメソッドからActionを呼び出すようにする
enum InfomationListCore {
    struct Observer {
        enum Action {
            case fetched([InfomationEntity])
        }

        var create: (AnyHashable, NSManagedObjectContext) -> Effect<Action, Never> = { _, _ in
            .none
        }

        static let live: Self = {
            var observer = Self()

            observer.create = { id, viewContext in
                Effect.run { subscriber in
                    let frc = createFetchedResultsController(viewContext)
                    let delegate = FetchedResultsDelegate(subscriber)

                    dependencies[id] = Dependencies(
                        fetchedResultsController: frc,
                        fetchedResultsDelegate: delegate                        
                    )
                    return AnyCancellable {}
                }
            }
            return observer
        }
    }
}

delegateを実装するclass

  • NSFetchedResultsControllerDelegateを実装するclass
    • delegateメソッドからSubscriberのsendを呼び出す
extension InfomationListCore {

    class FetchedResultsDelegate: NSObject, NSFetchedResultsControllerDelegate {

        private let subscriber: Effect<Observer.Action, Never>.Subscriber

        init(_ subscriber: Effect<Observer.Action, Never>.Subscriber) {
            self.subscriber = subscriber
        }

        // delegateのメソッド
        func controllerDidChangeContent(
            _ controller: NSFetchedResultsController<NSFetchRequestResult>
        ) {
            let contents = controller.fetchedObjects as! [InfomationEntity]
            subscriber.send(.fetched(contents))
        }
    }
}

iOS13からCore Dataのdelegateはsnapshotを取り出せるのもある。snapshotを取り出してCollectionView側で差分をチェックしてくれて反映さそうなのでそっちでもいい。


まとめ

  • 前提
    • ActionはViewStore以外でも動作する
      • ActionはActionを呼べる
        • 同列のActionだけでなく、親から子、子から親でも可能
    • TCAのEffectはCombine.Publisherに準拠している
      • Combine.Publisher/Subscriberはイベントを完了するまで何度でも呼び出せる
  • TCAからdelegateで自動で何かを監視して処理が実行される仕組みを使う
    • その仕組の中でEffectからSubscriberが取り出せるのでイベントを呼び出せる

一つのActionをきっかけにdelegateを作成して監視させれば、そこからActionを何度も呼び出すことができる。

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

Kotlin Multiplatform Mobileで作ったプロジェクトで実機ビルドができない場合の対応

概要

プラグインKotlin Multiplatform Mobileを利用してプロジェクトを作った際、実機ビルドができないとなった時の対応。

エラー内容としては下記。

${Path}/iosApp.xcodeproj Building for iOS Simulator, but the linked and embedded framework 'shared.framework' was built for iOS.

原因

AndroidとiOS側で共有されている'shared.framework'がiOS Simulator用にビルドされていて、実機では使えないという話。

FWは以下のような処理を経て生成される。
具体的にはConfigurationを基に各ターゲット向けに生成されたFWをコピーし、xcode-frameworksという名前にしている。

shared/build.gradle.kts
val packForXcode by tasks.creating(Sync::class) {
    group = "build"
    val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
    val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator"
    val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64"
    val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode)
    inputs.property("mode", mode)
    dependsOn(framework.linkTask)
    val targetDir = File(buildDir, "xcode-frameworks")
    from({ framework.outputDirectory })
    into(targetDir)
}

Gradleタスクを呼び出しは、iOSのBuild Phaseで行われている。
問題があるのはここ。

cd "$SRCROOT/.."
./gradlew :shared:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}

Xcode10で最適化のため新しいbuild systemに移行された。
その際build phaseでの設定方法やその処理内容に変更が入り、ファイルに変更がなければbuild phaseのスクリプトも実行されなくなった。

詳しい変更内容は下記で確認。

そのため、shared:packForXCodeにおいても一度Simlulator向けにビルドすると、FWは存在しているので実行されなくなる。
xcode-frameworksを削除すれば、実機向けにもビルドできるが逆も然りで、この場合Simlulator向けにビルドできなくなる

${Path}/iosApp.xcodeproj Building for iOS Simulator, but the linked and embedded framework 'shared.framework' was built for iOS.

対応

Build Phaseではなく、SchemaのPre-ActionsでpackForXCodeを実行する。
注意が必要なのは Provided build settings fromも変更しておくこと。

Screen Shot 2021-01-24 at 9.48.25.png

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

【iOS】個人開発で使って気分がよかったライブラリ5選【個人開発】

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

【iOS/Android】ネイティブアプリ開発をしたい人への上達マップ【初心者】

想定読者:これからネイティブアプリを開発したい人

想定読者に向けて、上達の全体像をお伝えします。
新人や駆け出しエンジニアの方が自分がどのあたりにいるか把握でき
モチベーションアップになれば幸いです。

まずは入門書の前半くらい

画面のレイアウトを作れるようになる
数字や文字列を表示できるようになる
ボタンを作る
ボタンを押したら、数字が増えたり減ったりするカウンターを作れるようになる
画面遷移ができるようになる
画面遷移の時に値を渡せるようになる

入門書の後半くらい

値を保存できるようになる。
値を呼び出せるようになる。
データベースにデータを保存できるようになる。
データベースからデータを読み出せるようになる。

色や動きなどを実装できるようになる

結構大変になってくる

通信処理ができるようになる
エラーハンドリングができるようになる

お、そろそろ実務の駆け出しくらいには行けるか?

綺麗にレイアウトを組めるようになる
(AutoLayoutなど)
デザインガイドラインを理解する
非同期通信ができるようになる

アプリの審査に提出できる
権限周りが大変。

プロレベル。

アーキテクチャについて詳しくなる
 MVCとMVVMくらいまで
共通化ができるようになる
レイヤードアーキテクチャにて理解・実装

リアクティブに動けるようにする
(RxSwiftやJetPackなど)

プラスアルファ

CI/CDの構築
適切なアーキテクチャの選定
UX/UIデザインを考えた実装の可能不可能の判断
営業やデザイナーとのコミュニケーション

まとめ

箇条書きですが、こんな道筋です。
結構大変になってくるレベルで挫折や、応募してくる方がいらっしゃいますが
もう少しです。どうにかリリースまで漕ぎ着けてください!
いっしょにプロのネイティブアプリエンジニアとしていいもの作りましょう!

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

iOSアプリリリースで、誰も教えてくれなかった9つのこと

今週、iOSアプリをリリースしました。
Purelistという、とびきり書きやすいリスト型のライティングアプリです。
たぶん使えば言っている意味がわかってもらえるかと思います。

リリース後1週間、いくつか想定外のことがあったのでリストにしました。
これを読んで少しでもプロモーションやアプリストアのスクショの改善に役立てていただけると幸いです。

1. リリース後にプロモーションしない限り、ほぼ誰にも見つけてもらえない
2. プロモーションしてもほぼ埋もれるし、インストールされない
3. レーティングは国別に表示される
4. サブタイトルは結構重要
5. プロモーション用テキストと概要は同じ場所に表示される
6. Store用アイコンは透過していなくても透過してるよって注意される
7. App Analyticsの更新時間はバラバラ
8. オプトインユーザーが少ないのでデータを拾いにくい
9. フィードバックはApp Storeでもらいにくい

1. リリース後にプロモーションしない限り、ほぼ誰にも見つけてもらえない

リリースしてから12時間、何もせずにいたら自分(デザイナー)とエンジニアしかインストールしませんでした。

その後、Product Hunt、Twitter広告、Facebook広告、Apple Search Ads、Instagramに流してやっと何百かインストールしてもらえました。

2. プロモーションしてもほぼ埋もれるし、インストールされない

USやEU、インドを中心とした話ですが、Twitter広告とFacebook広告は配信してクリックがあってもインストールまではほぼ到達しません。
3%〜4%くらいしかインストールしません。
クリエイティブやプロモーション文と実際のアプリストアに差を感じたのかもしれませんが、個人的にはレーティングがないとほぼインストールに至らないという仮説を立てています。

App StoreでリリースしたらApple Search Adsを使うのが一番コスパいいです。

Product Huntはよほど世界的に愛されるサービスか、100人くらい継続的にProduct Huntを利用している知り合いがいれば有利です。
知り合いがいない場合は土曜日か日曜日に情報を発信するのが有利です。
サイトを見る人も少ないですが、投稿する人も少ないので、その分TOPに載りやすくなります。
できれば日曜日の方が、月曜日にサイトを閲覧した人にも届きやすいのでいいと思います。

3. レーティングは国別に表示される

上にも仮説として書きましたが、レーティングがないとインストールに至る確率は下がると感じています。
そのため、レーティングを知人に頼んで稼ぐことができても、それは他の国では全く見られていないということです。

ターゲットとしている国が母国ではないのであれば、なるべくターゲット国を絞った状態でプロモーション等をすることをお勧めします。
でないとレーティングが分散し、誰からも微妙にしか相手にされていないアプリになります。

4. サブタイトルは結構重要

App Storeに掲載される、サブタイトルはSEO的にかなり重要になります。
ここに検索キーワードとなるような語句を入れておいた方がいいです。

Apple Search AdsのTOPにも記載してある通り、検索結果画面からそのままインストールする人は65%ほどいます。

検索画面に表示されるのはスクショ、サブタイトル、レーティング、タイトルです。
この4つはかなり重要な情報なので慎重に言葉選びや画像選びをすることをお勧めします。

5. プロモーション用テキストと概要は同じ場所に表示される

App Store Connectをみるとプロモーション用テキストと概要の入力欄があるが、同じ位置に表示されるものなので、同じ文章を書くとおかしくなります(おかしくなりました)

Asset 2.png

Asset 1.png

6. Store用アイコンは透過していなくても透過してるよって注意される

Macのプレビューで画像を開いて保存時に透過のチェックを外すのを忘れずに。。。

7. App Analyticsの更新時間はバラバラ

毎日20時に更新されると思ってたけど全然間違いでした。
16:00に更新されることもあれば、朝4:00になっても更新されないことがあります。
諦めて寝ましょう。

スマホもパソコンもバラバラでした。

8. オプトインユーザーが少ないのでデータを拾いにくい

リテラシー高いのはいいことだけども!!
オプトインユーザー、つまりAppleへのデータの提供をOKと言ってくれてるユーザーですが、かなり少ないです。
Appleのアナリティクスしか使用していない場合、十分なデータを取るには十分なオプトインユーザーの数が必要です。
なので、データにならないことがだいぶあります。

9. フィードバックはApp Storeでもらいにくい

USやEUは個人主義が進んでいるのか、自分のメディアにフィードバックを記載しますが、App Storeには書いてくれません。
たくさんエゴサしてフィードバックを見つけるしかないです。
なのでエゴサしやすい名前をつけることをお勧めします。


以上です。
また期間が空いたら追加したり別の内容を書いたりすると思います。

Purelist

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