20200204のiOSに関する記事は15件です。

SwiftUIとRealmの連携方法

はじめに

SwiftUIとRealmを連携してライブアップデートを効かせる方法を記載します。

要点

  • Realmオブジェクトはライブアップデート機能を備えており、該当のインスタンスは常に最新の値で更新されている
  • Realmオブジェクトを @Published で保持したとしても、値に変更があった際に自動で更新されるようにはならない
  • observeメソッドを利用してSwiftUIに再レンダリングの指示を与えることで、常に最新の値がUIに反映されるようにすることができる

Realmのインストール

RealmはSwiftPackageManagerに対応しているので、今回はこれを利用してインストールします

  1. Dependencyを追加する
    image

  2. realm-cocoa を選択する
    image

  3. バージョンを指定する
    image

  4. プロダクトを選択する(両方選択します)
    image

  5. import RealmSwift を記述すれば利用可能になる

Entityの用意

  • まずは準備としてRealmで保持するデータの型を定義します。
  • 今回は動作検証用として、以下の挙動を行うようにしています
    • setUpメソッドを実行すると2秒ごとにデータを一度全て削除し、ランダムな名称が設定されたItemデータが再セットされる
import RealmSwift

class ItemEntity: Object, Identifiable {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""

    override class func primaryKey() -> String? { "id" }
    override class func indexedProperties() -> [String] { ["id"] }

    private static var realm = try! Realm()

    static func setUp() {
        Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (timer) in
            try! realm.write {
                realm.deleteAll()
                realm.add(createFixture(), update: .modified)
            }
        }
    }

    static func all() -> Results<ItemEntity> {
        realm.objects(ItemEntity.self)
    }

    private static func createFixture() -> [ItemEntity] {
        (0..<10)
            .map { _ in (0..<1000).randomElement()! }
            .map { number -> ItemEntity in
                let item = ItemEntity()
                item.id = "\(number)"
                item.name = "item\(number)"
                return item
            }
    }
}

ObservableObjectを利用する

  • Entityを用意したらViewModel(ObservableObject)を介してRealmのデータを取得するようにします
  • しかし、このコードは最初にフェッチしたデータしか表示されません。
    • @Published で変更が監視されるのはその変数の値自体が変わったタイミングなので、RealmのResultsインスタンスの内部状態に変更があっても、Viewの再レンダリングが行われないためです。
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            // DBに変更があってもSwiftUIはその変更を検知できず、UIは初期表示から更新されない
            ForEach(viewModel.itemEntities) { itemEntity in
                Text(itemEntity.name)
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
}

image

対応方法

  • それではDBのデータに変更があった際に、Viewを再レンダリングさせるにはどうすれば良いでしょうか
  • Realmのオブジェクトが持つ observe メソッドを利用することでこれを実現することができます
  • このメソッドを利用すると、DBのデータに変更が起きるたびに受け渡したクロージャが実行されるので、そのタイミングをフックしてObservableObjectの状態を更新してあげれば期待した挙動をさせることができます
entity.observe { (change) in
    switch change {
    case let .initial(results):
        // do something
    case let .update(results, deletions, insertions, modifications):
        // do something
    case let .error(error):
        // do something
    }
}
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.itemEntities) { (itemEntity: ItemEntity) in
                if itemEntity.isInvalidated {
                    EmptyView()
                } else {
                    Text(itemEntity.name)
                }
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        // DBに変更があったタイミングでitemEntitiesの変数に値を入れ直す
        notificationTokens.append(itemEntities.observe { change in
            switch change {
            case let .initial(results):
                self.itemEntities = results
            case let .update(results, _, _, _):
                self.itemEntities = results
            case let .error(error):
                print(error.localizedDescription)
            }
        })
    }

    deinit {
        notificationTokens.forEach { $0.invalidate() }
    }
}

※ もしくは、 @Published を利用せずに手動で状態の変更を通知する方法でも同様の挙動を実現できます。
この方法を利用すれば、プロパティの値を置換することなく再レンダリングをさせることができます。

import Combine
class Store: ObservableObject {
    var objectWillChange: ObservableObjectPublisher = .init()
    private(set) var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        notificationTokens.append(itemEntities.observe { _ in
            // SwiftUIに再レンダリングが必要なことを伝える
            self.objectWillChange.send()
        })
    }

    // ...
}

この実装によって、自動更新の挙動を実現することができました。(gif参照)
image

(おまけ)EnvironmentObjectを利用する

  • 上記の例はObservableObjectを利用しましたが、EnvironmentObjectとしてデータを管理したいというケースもあると思います
  • この変更はとても簡単で以下の修正を入れるだけです
    • (ViewModelをStoreにリネーム)
    • Viewが保持するインスタンスを @ObservedObject から @EnvironmentObject に変更
    • Viewの初期化時に environmentObject Modifierでデータを保持するオブジェクトを受け渡す
  • これによりEnvironmentObjectがバインドされたViewは、DBに更新があった時に全て自動的に更新されるようになります
- class ViewModel: ObservableObject {
+ class Store: ObservableObject {
    // ...
}
struct ContentView: View {
    - @ObservedObject(initialValue: ViewModel()) private var viewModel: ViewModel
    + @EnvironmentObject private var store: Store
let contentView = ContentView()
+    .environmentObject(Store())

サンプルコード

https://github.com/chocoyama/SwiftUI-Realm

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

SwiftUIとRealmを連携してUIを自動更新する

はじめに

SwiftUIとRealmを連携してライブアップデートを効かせる方法を記載します。

要点

  • Realmオブジェクトはライブアップデート機能を備えており、該当のインスタンスは常に最新の値で更新されている
  • Realmオブジェクトを @Published で保持したとしても、値に変更があった際に自動で更新されるようにはならない
  • observeメソッドを利用してSwiftUIに再レンダリングの指示を与えることで、常に最新の値がUIに反映されるようにすることができる

Realmのインストール

RealmはSwiftPackageManagerに対応しているので、今回はこれを利用してインストールします

  1. Dependencyを追加する
    image

  2. realm-cocoa を選択する
    image

  3. バージョンを指定する
    image

  4. プロダクトを選択する(両方選択します)
    image

  5. import RealmSwift を記述すれば利用可能になる

Entityの用意

  • まずは準備としてRealmで保持するデータの型を定義します。
  • 今回は動作検証用として、以下の挙動を行うようにしています
    • setUpメソッドを実行すると2秒ごとにデータを一度全て削除し、ランダムな名称が設定されたItemデータが再セットされる
import RealmSwift

class ItemEntity: Object, Identifiable {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""

    override class func primaryKey() -> String? { "id" }
    override class func indexedProperties() -> [String] { ["id"] }

    private static var realm = try! Realm()

    static func setUp() {
        Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (timer) in
            try! realm.write {
                realm.deleteAll()
                realm.add(createFixture(), update: .modified)
            }
        }
    }

    static func all() -> Results<ItemEntity> {
        realm.objects(ItemEntity.self)
    }

    private static func createFixture() -> [ItemEntity] {
        (0..<10)
            .map { _ in (0..<1000).randomElement()! }
            .map { number -> ItemEntity in
                let item = ItemEntity()
                item.id = "\(number)"
                item.name = "item\(number)"
                return item
            }
    }
}

ObservableObjectを利用する

  • Entityを用意したらViewModel(ObservableObject)を介してRealmのデータを取得するようにします
  • しかし、このコードは最初にフェッチしたデータしか表示されません。
    • @Published で変更が監視されるのはその変数の値自体が変わったタイミングなので、RealmのResultsインスタンスの内部状態に変更があっても、Viewの再レンダリングが行われないためです。
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            // DBに変更があってもSwiftUIはその変更を検知できず、UIは初期表示から更新されない
            ForEach(viewModel.itemEntities) { itemEntity in
                Text(itemEntity.name)
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
}

image

対応方法

  • それではDBのデータに変更があった際に、Viewを再レンダリングさせるにはどうすれば良いでしょうか
  • Realmのオブジェクトが持つ observe メソッドを利用することでこれを実現することができます
  • このメソッドを利用すると、DBのデータに変更が起きるたびに受け渡したクロージャが実行されるので、そのタイミングをフックしてObservableObjectの状態を更新してあげれば期待した挙動をさせることができます
entity.observe { (change) in
    switch change {
    case let .initial(results):
        // do something
    case let .update(results, deletions, insertions, modifications):
        // do something
    case let .error(error):
        // do something
    }
}
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.itemEntities) { (itemEntity: ItemEntity) in
                if itemEntity.isInvalidated {
                    EmptyView()
                } else {
                    Text(itemEntity.name)
                }
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        // DBに変更があったタイミングでitemEntitiesの変数に値を入れ直す
        notificationTokens.append(itemEntities.observe { change in
            switch change {
            case let .initial(results):
                self.itemEntities = results
            case let .update(results, _, _, _):
                self.itemEntities = results
            case let .error(error):
                print(error.localizedDescription)
            }
        })
    }

    deinit {
        notificationTokens.forEach { $0.invalidate() }
    }
}

※ もしくは、 @Published を利用せずに手動で状態の変更を通知する方法でも同様の挙動を実現できます。
この方法を利用すれば、プロパティの値を置換することなく再レンダリングをさせることができます。

import Combine
class Store: ObservableObject {
    var objectWillChange: ObservableObjectPublisher = .init()
    private(set) var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        notificationTokens.append(itemEntities.observe { _ in
            // SwiftUIに再レンダリングが必要なことを伝える
            self.objectWillChange.send()
        })
    }

    // ...
}

この実装によって、自動更新の挙動を実現することができました。(gif参照)
image

(おまけ)EnvironmentObjectを利用する

  • 上記の例はObservableObjectを利用しましたが、EnvironmentObjectとしてデータを管理したいというケースもあると思います
  • この変更はとても簡単で以下の修正を入れるだけです
    • (ViewModelをStoreにリネーム)
    • Viewが保持するインスタンスを @ObservedObject から @EnvironmentObject に変更
    • Viewの初期化時に environmentObject Modifierでデータを保持するオブジェクトを受け渡す
  • これによりEnvironmentObjectがバインドされたViewは、DBに更新があった時に全て自動的に更新されるようになります
- class ViewModel: ObservableObject {
+ class Store: ObservableObject {
    // ...
}
struct ContentView: View {
    - @ObservedObject(initialValue: ViewModel()) private var viewModel: ViewModel
    + @EnvironmentObject private var store: Store
let contentView = ContentView()
+    .environmentObject(Store())

サンプルコード

https://github.com/chocoyama/SwiftUI-Realm

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

iOSとmacのunityのnative pluginを作る

iOSとmacのunityのnative pluginを作る

unity editorで使えるmacOS用のbundleとiOS用のFrameworkのnative pluginを作成する手順です。
Bluetoothや動画作成など、Unityに実装されていない機能をiOS, macOSの機能を使うことで実現します。
UnityPluginXcodeTemplateをベースに作成します。
UnityPluginXcodeTemplateを使うことで、unityのnative pluginを作成するために必要なxcodeの複雑な設定の必要がなくなり、editorで動作確認できるbundleと実機用のframeworkが同時に開発できます。

今回作成するnative pluginの目的

  • unity(C#)には加算演算が実装されていない(設定)です。そこで、swiftの加算演算子を使って加算演算を実現するnative pluginを実装します。
  • 手順に着目しているので、今回は加算演算のみの実装ですが、同じ手順でbluetoothなどのnative pluginを作成しているので、参考になれば嬉しいです。
  • iOS, macOSで使えるbluetoothのnative plugin: https://github.com/fuziki/UnityCoreBluetooth

作成するC#クラス

  • NativeSuperCalculatorクラスです。
  • 加算演算ができるAddメソッドを持っています。
NativeSuperCalculator.cs
public class NativeSuperCalculator {
    public static int Add(int l, int r);
}

作成するSuperCalculator Xcodeプロジェクト

5つのターゲットがあります

Target Description
SuperCalculator.framework iOS BuildとExample_iOSでの動作確認に使用するframework
SuperCalculator_macOS.framework macOS_Exampleで動作確認に使用するframework
SuperCalculator_bundle.bundle macのUnity Editorで動作確認に使用するbundle
Example_iOS 作成した実装をUnityに組み込む前にiOSで動作確認するために使用する
Example_macOS 作成した実装をUnityに組み込む前にmacOSで動作確認するために使用する

作成するswiftクラス

SuperCalculatorクラスです。
加算演算ができるaddメソッドを持っています。

SuperCalculator.swift
@objcMembers
public class SuperCalculator: NSObject {
    public static func add(l: Int, r: Int) -> Int {
        return l + r  //swiftにしか加算演算子がない(設定)
    }
}

Xcode プロジェクトを作成する

1. UnityPluginXcodeTemplateをcloneしてrename

git clone https://github.com/fuziki/UnityPluginXcodeTemplate
mv UnityPluginXcodeTemplate/ SuperCalculatorPlugin/
cd SuperCalculatorPlugin

2. setting.ymlを編集

setting.yml
PROJECT_NAME: SuperCalculatorPlugin
FRAMEWORK_IOS: SuperCalculator
FRAMEWORK_MACOS: SuperCalculator_macOS
BUNDLE_MACOS: SuperCalculator_bundle
EXAMPLE_IOS: Example_iOS
EXAMPLE_MACOS: Example_macOS

3. xcodeprojを作成する

xcodegenを使用して、Xcodeディレクトリ下にxcodeprojを作成します。

make setup
open Xcode/SuperCalculatorPlugin.xcodeproj/

Swiftで機能を実装する

1. SuperCalculator.swiftの実装

  1. xcodeプロジェクトのNavigator AreaからLibrary/Sourcesを右クリック→ new file...を選択
  2. swift fileを選択し、next
  3. ファイル名はSuperCalculatorを指定、targetはSuperCalculator, SuperCalculator_macOS, SuperCalculator_bundleを選択してcreate
    スクリーンショット 2020-02-04 22.26.50.png

  4. SuperCalculatorクラスを実装する

  5. objective-cから呼び出すために@objcMembers属性をつけます。

SuperCalculator.swift
@objcMembers
public class SuperCalculator: NSObject {
    public static func add(l: Int, r: Int) -> Int {
        return l + r//swiftにしか加算演算子がない(設定)
    }
}

2. Unityからの呼び出し部分の実装

  1. NativePlugin.hの実装
  2. Unityのnative pluginはcの呼び出しなので、cのインタフェースを作成します。
  3. superCalculator_addを追加します。
NativePlugin.h
  #ifdef __cplusplus
  extern  "C" {
  #endif
     int add_one(int num);
+    int superCalculator_add(int l, int r);  //追加する
  #ifdef __cplusplus
  }
 #endif
  1. NativePlugin.mmの実装
  2. superCalculator_addが呼ばれたら、swiftのSuperCalculator.addを呼び出して計算します。
NativePlugin.mm
int superCalculator_add(int l, int r) {
    return (int)[SuperCalculator addWithL:l r:r];
}

3. macOS, iOSでの動作確認(optional)

  • viewDidLoadでsuperCalculator_addを呼び出して、正しく動作することを確認する。
    ※ iOSは実機のみサポートです。
ViewController.swift
override func viewDidLoad() {
    super viewDidLoad()
    // Do any additional setup after loading the view.
    let res = superCalculator_add(32, 42)
    print("res: \(res)")
}

UnityのNative Pluginとして導入する

1. iOS向けframeworkのビルド

  • schemeからSuperCalculatorを選択して、ビルドする。 スクリーンショット 2020-02-04 22.37.51.png

2. macOSのunity editor向けbundleのビルド

  • schemeからSuperCalculator_bundleを選択して、ビルドする。 スクリーンショット 2020-02-04 22.38.06.png

3. 成果物の確認

  • 4つ作成されているはず
    • Xcode/Out/Assets/SuperCalculatorPlugin/Plugins/iOS/SuperCalculator.framework.meta
    • Xcode/Out/Assets/SuperCalculatorPlugin/Plugins/iOS/SuperCalculator.framework
    • Xcode/Out/Assets/SuperCalculatorPlugin/Plugins/macOS/SuperCalculator_bundle_xxxxxxx.bundle
    • Xcode/Out/Assets/SuperCalculatorPlugin/Scripts/SuperCalculator_bundle.cs

4. 成果物をUnityにコピーする

  • Xcode/Out/Assets/SuperCalculatorPlugin/. を Unity/LibraryUser/Assets/Plugins/. にコピーする
mkdir Unity/LibraryUser/Assets/Plugins/SuperCalculatorPlugin
cp -r Xcode/Out/Assets/SuperCalculatorPlugin/. Unity/LibraryUser/Assets/Plugins/SuperCalculatorPlugin/.

Unityからnative pluginを使う実装

1. NativeSuperCalculator.csを作成し、NativeSuperCalculatorクラスを実装する

NativeSuperCalculator.cs
using System.Runtime.InteropServices;
namespace SuperCalculatorPlugin
{
    public class NativeSuperCalculator
    {
        [DllImport(SuperCalculator_bundle.IMPORT_TARGET)]
        private static extern int superCalculator_add(int l, int r);
        public static int Add(int l, int r) {
             return superCalculator_add(l, r);
        }
    }
}

2. NewBehaviourScriptなどで呼び出してみる

NewBehaviourScript.cs
using SuperCalculatorPlugin;
public class NewBehaviourScript : MonoBehaviour {
    void Start ()
    {
        int res = NativeSuperCalculator.Add(32, 45);
        Debug.Log("res: " + res);
    }
}

おわりに

Unityに実装されていない(設定の)加算をnative pluginを使って実現するプラグインを作成しました。
iOSの実機のみで使用する場合は、.mmファイルをベタ置きしてunityのビルドしたxcode projectにブリッジヘッダを設定すると言う方法もあるのですが、ブリッジヘッダの設定や、editorで動かないなど手間がかかるので、個人的にはbundleとframeworkを作りたい派です。
同様の手順でbluetoothや、ble keyboard, mp4作成など、unityに実装されていない、楽しい機能のネイティブプラグインを作成できるので、一度試してみて頂けると嬉しいです!

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

シャッター音なしのアプリを作ってみた

作るに至ったわけ

日本や韓国などではスマホ(ケータイ含め)で写真を撮影しようとするとシャッター音が鳴りますが、海外で販売されたスマホ(iPhone含め)はシャッター音は鳴りません。インスタなどでレストランなどの静かなところで撮影する時など、あの音が鳴ると気を使いますよね。マナーからだという意見もありますが、スクリーンショットもなる必要はありません。法律で決まってるわけでないので、徐々にこの慣習がなくなっていくといいのになと思います。

というわけで、iOS向けにマナーモード時はシャッター音がならないカメラアプリを作ってみました。

無音カメラを作る方法

  • ビデオ撮影モードで静止画をキャプチャする方法

割とこの方法は一般的なようでぐぐると多く見つかります。ただ、静止画モードではないため解像度が劣ったりといろいろ制限があります。

  • 逆位相の音を再生する方法

ノイズキャンセリングと同じ方法ですが、ジャストのタイミングで再生しないといけないため、難易度は高いようです。

  • シャッター音のサウンドファイルを削除する方法

Jailbreakしかできません。
などなど。今回、どうやったかはまだ言えません。

審査を乗り越えるには

Appleの審査が通るまで約1ヶ月かかりました。基本的には、ユーザーに気付かないような録音、録画、撮影などは許可できないというもの。シャッター音を消すことはできませんと。特に、アプリ名からも連想させてはいけませんと。最初は「サイレントカム」っていう名前でした。

だって、AppStore見ると「無音カメラ」とかもろいってるアプリだってあるわけで、なんで???って感じでした。アプリの説明にも思いっきり書いてあるし、、神であるAppleが言うのだから仕方ありません。

未だ謎ですが、

  • アプリ名には「無音」「サイレント」と言った連想するキーワードは入れない
  • シャッター音をなるモードも用意する(今回のようにマナーモード以外はシャッター音鳴ります)

と言うのが今回の審査でわかりました。

使ったフレームワーク

  • AVFoundation.framework

ちょっと面倒でしたが、たまたまみたサンプルがこれだったのと、撮影画面とかをカスタマイズできたほうがいいかなと思ったのが理由です。

そしてリリース

C'zCam
https://apps.apple.com/jp/app/czcam/id585273753

静か目、静かカメラ、静かカメ、、、というわけで、C'zCamです。
よかったら試してみてください。無料です。

参考

https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/avcam_building_a_camera_app

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

【CocoaPods】iOSのライブラリをForkして使う

iOSアプリのライブラリを一部カスタマイズして使う機会があったので。

フォーク

まず普通にFork。
https://help.github.com/ja/github/getting-started-with-github/fork-a-repo
GitHub公式練習用リポジトリSpoon-Knifeを使って説明。

このボタンを押して
image.png
こうなれば成功。
image.png

Fork元の変更を楽に取り込むローカル構成手順もあるが割愛。
https://help.github.com/ja/github/getting-started-with-github/fork-a-repo#step-1-set-up-git

Podfile

ライブラリを修正したら、Podfileで本来このように書くものを…

  pod 'Spoon-Knife'

このようにfork先のrepoとbranchを指定してpod install

  pod 'Spoon-Knife', :git => 'git@github.com:{{ your name }}/Spoon-Knife.git', :branch => '{{ your branch }}'

参照 : https://guides.cocoapods.org/using/the-podfile.html#from-a-podspec-in-the-root-of-a-library-repo

ただし元のライブラリが既にインストール済みの場合、リポジトリのみの変更は解釈されないので一度この行を消しての再インストールが必要だった。

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

NavigationBarのBackボタンの見た目を変えずにイベントをつける

はじめに

久々に投稿します!

NavigationBarの「<」Backボタンにイベントをつけたいと思い、調査をしましたが
見た目を変えずにイベントを付与することができる情報がなかなか見つからなかったので
備忘としてまとめてみます:point_up:

:warning::warning:iOS12以上をサポートしている前提で調査をしたため、SwiftUIは使用していません。:warning::warning:

開発環境

以下の環境で開発しました。

Tool Version
Xcode 11.2.1 (11B500)

やりたいこと

今回実現したいことは以下の通りです。

  1. NavigationBarのBackボタンタップ時にアラートを表示したい
  2. Backボタンの見た目は変えたくない
  3. Backボタンで戻る際のアニメーションも変えたくない

1番のみであれば、Backボタンを非表示にしてleftBarButtonItemに自作のBackボタンを追加すれば実現できそうですが、
2番3番の見た目を変えないというのが難しいです。

実現に向けたアプローチ

実現方法を2つ考えました。
1. NavigationBarのSubviewsを再帰的に取得して、どうにか「<」の画像を手に入れて自作のBackボタンに使用する
2. Backボタンのイベントを取得して、自作のイベントで上書きをする

1番のアプローチでは、画像取得はできましたが結局自分で画像やタイトルの位置を調整する必要が出てきました:no_good_tone1:
2番のアプローチで実現することができました。

具体的には、UIViewControllerにExtensionを実装します。
navigationController?.navigationBarのSubviewsを再帰的に探索してUIControlを取得し、
戻るイベントを削除し、新しいイベントを登録します。

実装

再帰的にSubViewsを取得する

以下を参考にさせていただきました。
Swiftで再帰的なサブビューの取得とUI層のユニットテストへの応用

UIView+RecursiveSubviews.swift
import UIKit

extension UIView {
    /// 再帰的にサブビューを取得する
    var recursiveSubviews: [UIView] {
        return subviews + subviews.flatMap { $0.recursiveSubviews }
    }

    /// UIViewの特定サブクラスのビューを取得する
    func findViews<T: UIView>(subclassOf: T.Type) -> [T] {
        return recursiveSubviews.compactMap { $0 as? T }
    }
}

UIControlのタップイベントをクロージャで登録する

ボタン等のイベントの登録addTarget(_:action:for:)をクロージャでするためのExtensionを作成しました。

UIControl+Tap.swift
import UIKit

typealias TapEvent = () -> Void

extension UIControl {

    /// タップイベントをクロージャで登録する
    func tap(action: @escaping TapEvent) {
        self.eventListener(controlEvents: .touchUpInside, forAction: action)
    }

    func eventListener(controlEvents control: UIControl.Event, forAction action: @escaping(() -> Void)) {
        self.actionHandler(action: action)
        self.addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}

private extension UIControl {

    func actionHandler(action: (TapEvent)? = nil) {
        struct ActionHolder {
            static var action :(TapEvent)?
        }
        if let action = action {
            ActionHolder.action = action
        } else {
            ActionHolder.action?()
        }
    }

    @objc func triggerActionHandler() {
        self.actionHandler()
    }
}

NavigationBarのBackボタンにイベントを登録する

上記の2つのExtensionを使用して、
NavigationBarのBackボタンにイベントを登録するUIViewControllerのExtensionを作成しました。

UIViewController+AddNavigationBackEvent.swift
import UIKit

extension UIViewController {

    /// NavigationBarのBackボタンにイベントを登録する
    func addNavigationBackEvent(action: @escaping TapEvent) {

        guard let controls = navigationController?.navigationBar.findViews(subclassOf: UIControl.self) else {
            return
        }

        for control in controls {
            if control.allTargets.isEmpty {
                continue
            }
            control.removeTarget(nil, action: nil, for: .allEvents)
            control.tap(action: action)
            break
        }
    }
}

使い方

対象のViewControllerのviewDidLayoutSubviews()で呼び出します。
viewDidLoadで呼び出すと、Subviewsを取得できず、イベント登録ができません。

NavigationNextViewController
final class NavigationNextViewController: UIViewController {

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        self.addNavigationBackEvent { [weak self] in
            self?.confirmWhetherToBack()
        }
    }

    /// 前の画面に戻るかどうか確認するアラート表示
    private func confirmWhetherToBack() {
        let alert = UIAlertController(title: "確認", message: "入力内容が保存されていません。\n編集を終了しますか?", preferredStyle: .alert)

        // OKボタン
        alert.addAction(
            .init(title: "OK", style: .default, handler: { [weak self] _ in
                guard let `self` = self else {
                    return
                }
                self.navigationController?.popViewController(animated: true)
            })
        )

        // キャンセルボタン
        alert.addAction(
            .init(title: "Cancel", style: .cancel)
        )
        present(alert, animated: true)
    }
}

実行結果

イメージ通りの動きになりました:tada::tada:
demo.GIF

さいごに

やりたいことは実現できましたが、もう少し簡潔に実装できるのではないかと思っています:thinking:
もっと良い実装方法等ご存知でしたらご教示ください:innocent:

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

iOSアプリのライセンス生成ツール「LicensePlist」のセットアップ&操作方法

はじめに

ライセンス表示を手動で実装するのが手間なので、導入することにしました。

「LicensePlist」とは?

CarthageとCocoaPods、SwiftPMで管理しているライブラリのライセンス表示を自動的に生成するツールです。
設定ファイルを用意することで、任意のライブラリに対しても生成できるようです。

環境

  • OS:macOS Mojave 10.14.6
  • Swift:5.1.3
  • Xcode:11.3.1 (11C504)
  • LicensePlist:2.12.0

セットアップ

LicensePlistのインストール

Mintからインストールします。

Mintfile
+ mono0926/LicensePlist@2.12.0
$ mint bootstrap

その他の方法でインストールするには、公式ドキュメントをご参照ください。
https://github.com/mono0926/LicensePlist#installation

Settings Bundleの追加

プロジェクトに Settings.bundle がない場合、こちらの記事を参考に追加してください。
記事の内容と異なり、製品ターゲットのフォルダ内に作成していいと思います。

Settings Bundleの編集

まず、こちらの記事を参考に、デフォルトで配置されている内容を削除します。

以下のItemを追加
スクリーンショット_2020-02-04_17_08_43.jpg

Key Type Value
Type String Child Pane
Title String Licenses
Filename String com.mono0926.LicensePlist

TitleValue は任意の値でOKです。

操作方法

ライセンスファイルの生成

Cartfile ファイルや Pods フォルダがあるフォルダで license-plist コマンドを実行します。

$ mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle

以下の3ファイルが Settings.bundle 内に生成されます。
スクリーンショット_2020-02-04_17_23_53.jpg

XcodeGenを使っている場合、プロジェクトファイルを再生成した方がいいと思います。

私はMakefileを作成し、 make generate-license を実行すると「ライセンスファイルの生成→プロジェクトファイルの生成→CocoaPods用ワークスペースの生成」を一括で行うようにしています。

Makefile
PRODUCT_NAME := CurrencyConversion

.PHONY: generate-licenses
generate-licenses:
    mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle
    $(MAKE) generate-xcodeproj

.PHONY: generate-xcodeproj
generate-xcodeproj:
    mint run xcodegen xcodegen generate
    $(MAKE) install-cocoapods

.PHONY: install-cocoapods
install-cocoapods:
    bundle exec pod install

ライセンス表示までの画面遷移

ライセンスが表示されているか確認します。

[設定]アプリを起動
Simulator_Screen_Shot_-_iPhone_11_Pro_Max_-_2020-02-04_at_17_47_50.jpg

対象のアプリをタップ
Simulator_Screen_Shot_-_iPhone_11_Pro_Max_-_2020-02-04_at_17_47_55.jpg

「Licenses」をタップ
Simulator_Screen_Shot_-_iPhone_11_Pro_Max_-_2020-02-04_at_17_47_58.jpg

「Moya」をタップ
Simulator_Screen_Shot_-_iPhone_11_Pro_Max_-_2020-02-04_at_17_48_00.jpg

Moyaのライセンスが表示されます。
Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-04 at 17.48.04.png

バージョン管理から無視する(任意)

生成されたライセンスファイルをバージョン管理の対象外にします。

Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。

.gitignore
+ com.mono0926.LicensePlist/
+ com.mono0926.LicensePlist.latest_result.txt
+ com.mono0926.LicensePlist.plist

こちらの設定は任意です。
バージョン管理するのもありだと思います。     

おわりに

これでライセンス表示を実装する手間が省けます!
ただし、ライブラリの使用前にライセンスを確認するのは忘れないようにしましょう。

参考リンク

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

【swift】まず簡単なリファクタリングをしよう【初心者向け】

なぜ簡単なリファクタリングをするのか?

1.簡単なリファクタリングだけでも、コードの可読性が激増する。
2.難しい文法でリファクタリングをしようとすると、挫折する。
3.すぐ終わる。

大事なこと?

たまにリファクタリングをしたほうがいい。

リファクタリングで

  • 開発のしやすさ。
  • 説明のしやすさ。
  • コード修正のしやすさ。

を手に入れましょう✨

  • 説明のしやすさ。

これは、特に大事です。
誰かにアドバイスをもらうためコードを添付する際、
自分ですら読めないコードを誰が読めるのでしょう。

リファクタリングしないと??

初心者の方は初めてのアプリ(ポートフォリオなど)を作成する際、
『文法とかコードの可読性とかどうでもいいからとりあえず完成させたい!』という気持ちがあると思います。

しかし、コードが増えれば増えるほど

  • 以前書いたコードが理解できない
  • 変数名、メソッド名がパッと見わからない

などの問題が発生します。

簡単なリファクタリング集

わかりやすい命名をする

  • codicを使って命名する。
  • 命名規則を学んでみる→ 参考

三項演算子を使う

let hoge = true

if hoge {
    print("true")
} else {
    print("false")
}
// ↓三項演算子を使った表現。
hoge ? print("true") : print("false")

使用しないコードは削除する

コメントアウトして、放置しているコードはありがちですよね。
思い切って削除しましょう。

コメントは分かりやすく

パッと見で理解できるコメントを残しましょう。
参考

selfは省略可能

省略できる箇所は、するべきでしょう。
参考

参考文献

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

Selector完全攻略、そして初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について

前回の記事はNotificationCenterの基本でした。
その中で、難易度的に省いたトピックを今回書きます。

偉く長いタイトルにしましたが、トピックとしては二点で、

  • Selectorの話
  • AddTarget()/AddObserver()に変数渡したいときどうするか

を扱います。

実行環境

Swift: 5.1.3
Xcode: 11.3.1

Selector完全攻略

NotificationCenterのAddObserver()の引数の中に、Selectorが出てきます。
このSelector、僕はUI部品にAddTarget()するときの引数で最初に遭遇して、かなり扱いに苦しみました。

addTarget例
let button = UIButton()
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)

@objc func somefunction() { …… }

あるボタンをタップしたら、何か処理を動かすといったコードを書くとき、
こんな指定をすると思うんですが、正直疑問はたくさんありました。

  • @objcとは?
    • これはでもなんかお約束でつけなきゃいけないし、Xcodeが補完してくれるのでそんな問題じゃない
  • #Selectorってなんだ?
    • Selector()でもセレクタつくれるけど、どっち使ったらいいの?
  • なんか他のSwiftの構文と君全然違くない?
    • クロージャの渡し方と全然違うんだけど……
    • てか引数どうなってんの
    • これは何? 文字型? クロージャ?
  • たまにサンプルでsomefunction(_:)ってなってるの、何?
    • アメリカ式の顔文字?→何わろとんねん

当時の自分の疑問を解消するような形で、説明していきたいと思います。

Selectorとは

セレクタ(Selector)は、そもそもObjective-Cの概念です。
Swiftには純粋な意味でのセレクタは存在しませんが、
それだとObjective-Cのコードとの互換性で困るので、折衷案的にSwiftでもセレクタが使えます。

Objective-Cでは、下記のように指定します。

@selector ( method )

Swiftでは実は指定する方法が三通りあります。
よく使うと思われる順で、#Selector、Selector、文字列のみの三つです。

実装方法(引数なし)

セレクタで使おうとしているメソッドに引数がないときは指定が楽なので、まずはその前提で説明します。

共通サンプルコード

説明上必要なのはAddTarget()のところだけなんですが、動作確認用に使ったサンプルコード全部を載せときます。
自分で色々試してみたい方はお使いください。

サンプル
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100))
        button.backgroundColor = .black
        button.setTitle("Event", for: .normal)
        button.titleLabel?.textColor = .white
        view.addSubview(button)
        //以後の説明では↓この一行だけ使います
        button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
    }

    @objc func somefunction() {
        print("動かしたい処理を記述")
    }
}

#Selector

button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)

詳しくは→Selector Expression

Selector

button.addTarget(self, action: Selector("somefunction"), for: .touchUpInside) //No method declared with Objective-C selector 'somefunction'というワーニングが出る

Swiftで#とか@とかの接頭詞がついていると、あまり利用が推奨されないものという感覚があって、
Selectorの方が#SelectorよりSwiftらしいのか? と勉強しはじめの頃は思ったのですが、
Selectorに関しては#Selectorを使う方がいいと思います。
(異論あればコメントください。#なしのSelectorの存在意義が今のところ僕の中ではイマイチ飲み込めてません……)

XcodeからWrap the selector name in parentheses to suppress this warningという修正案が出るので、Fixを押すと、

え……
button.addTarget(self, action: Selector(("somefunction")), for: .touchUpInside)

なぜか二重カッコに訂正されます。
まあそれはいい(よくないが)んですが、一番キツイのは、#Selectorと違って、メソッド名のコンパイラチェックがないことです。

#Selector
button.addTarget(self, action: #selector(samufankusyon), for: .touchUpInside)
// →Use of unresolved identifier 'samufankusyon'

#Selectorは、メソッド名のチェックをしてくれるので、コンパイルエラーとなります。
一方で、

Selector
button.addTarget(self, action: Selector(("samufankusyon")), for: .touchUpInside)

Selectorは文字列なので存在しないメソッド名でも指定できてしまい、実行時エラーとなります。

Selectorで文字列からメソッドを呼び出す(Swift4.2)

文字列のみ

実は文字列だけ突っ込んでも、ワーニングは出ますが、なんか上手いこと解釈してくれて動きます。

button.addTarget(self, action: "somefunction", for: .touchUpInside) 
//No method declared with Objective-C selector 'somefunction'
//Replace '"somefunction"' with 'Selector("somefunction")' [Fix]

動作的にはSelector("somefunction")と一緒ですね。

実装方法(引数あり)

で、引数があるときの書き方。
ちょっと冗長になりますが、3パターン全部書いてみようと思います。
(初学者の頃網羅的に書いてくれてる記事がなくて辛かった思い出があるので)

その前に

セレクタで指定したメソッドの引数に何を渡せるかを決めるのは、そのセレクタを引数にとる関数が決めます。
ここではAddTarget()です。

#selector に複数の引数を持たせたいです
↑こちらの回答にある通り、AddTarget()でとれる引数は最大2つで、
しかもイベント発生時のUIButtonとUIEventのインスタンスしか取れません。

したがって、

button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)

@objc func somefunction(some: String, number: Int) {
    print("動かしたい処理を記述")
}

こんな書き方をすると、実行時エラーとなります。

ちなみに、NotificationCenterのAddObserver()の引数は、Notificationインスタンス1つです。

aSelector
Selector that specifies the message the receiver sends observer to notify it of the notification posting. The method specified by aSelector must have one and only one argument (an instance of NSNotification).

変数の型と中身は制約がありますが、変数名とセレクタの書き方は自由です。
具体的に見ていきましょう!

#Selector

button.addTarget(self, action: #selector(somefunction(sender:forEvent:)), for: .touchUpInside)
//button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
// ↑これでも呼び出し可

// sender/forEvent/eventなどの変数名は開発者によって変更可
@objc func somefunction(sender: UIButton, forEvent event: UIEvent) {
    print("動かしたい処理を記述")
    print(sender.titleLabel?.text) //Optional("Event")
    print(event) //<UITouchesEvent: 0x282e10640>……
}

初学者の頃よく混乱したネットに転がってるサンプルで、#selector(somefunction(_:))と指定している例がありますが、
あれは呼び出すメソッドの外部引数名を_で省略可としているため、こんな書き方ができるんですね〜

button.addTarget(self, action: #selector(somefunction(_:_:)), for: .touchUpInside)
@objc func somefunction(_ sender: UIButton, _ event: UIEvent) {……}

コロンで変数分けてるのがSwift的には気持ち悪いですが、Objective-Cの名残りだと思われます。

Selector  

Selectorの引数指定はちょっとしんどいです。
#Selectorみたいに、

button.addTarget(self, action: Selector("somefunction(sender:forEvent:)")), for: .touchUpInside)

でイケると思うじゃないですか。
実行時エラーでクラッシュします。
NSInvalidArgumentExceptionです。

Selectorで文字列からメソッドを呼び出す(Swift4.2)こちらを参考にしながら、色々試してみましたが、30分くらいクラッシュし続けました。

ダメだった例
Selector("somefunctionWithSender:ForEvent:")
Selector("somefunction")
Selector("somefunctionWithsender:forEvent:")
Selector("somefunctionwithsender:forEvent:")
Selector("somefunctionWithSender:Event:")
Selector("somefunctionWithSender:event:")
// 頭おかしくなりそう

メソッドの引数を一つにしてみたら動いたので、もうこれ書いてお茶を濁そうかなと思ったとき、参考にしていたページを見直して、気付きました。
あ、これ第二引数、一文字目小文字やん!!!!

正解
button.addTarget(self, action: Selector("somefunctionWithSender:forEvent:"), for: .touchUpInside)

正解はこう。
WithV1:V2:……みたいに引数を指定するのは、Objective-C方式です。
注意すべきは、第一引数はメソッド定義に関わらず一文字目を大文字に、
第二引数以降はメソッド定義に従う、というところでした。

うーんやっぱ#Selectorより無印Selectorの方がObjective-Cっぽいですよねえ。

文字列のみ

Selectorと一緒です。

NotificationCenter使うときのUserInfoの扱い方

AddTarget()を例に長々説明してきましたが、AddObserver()についても少し書かせてください。

AddObserver()はセレクタで指定したメソッドにNotification型を渡せるのですが、
NotificationにはuserInfoというプロパティがあり、postするときにちょっとしたデータを渡せます。

import Foundation

class Subject {
    let eventName: Notification.Name

    init(eventName: Notification.Name) {
        self.eventName = eventName
    }

    func post() {
        NotificationCenter.default.post(name: eventName, object: nil, userInfo: ["test": "Yeah"])
    }
}

class Observer {
    let eventName: Notification.Name

    init(eventName: Notification.Name) {
        self.eventName = eventName
        NotificationCenter.default.addObserver(self, selector: #selector(doWhenEventOccur), name: eventName, object: nil)
//        NotificationCenter.default.addObserver(self, selector: "doWhenEventOccurWithNotification:", name: eventName, object: nil)
    }

    @objc func doWhenEventOccur(_ notification: Notification) {
        print(notification.userInfo) //Optional([AnyHashable("test"): "Yeah"])
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

let eventName = Notification.Name("Event Occurs")
let observer = Observer(eventName: eventName)
let subject = Subject(eventName: eventName)
subject.post()

※このコードについての解説は前回の記事参照

ただ[AnyHashable: Any]型という、ちょっと扱いづらい辞書型で渡るので、正直使いづらいです。

初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について

以上で本題は終わりなんですが、関連して。
iOSアプリ開発に手を染めて、1〜2ヶ月くらい、やたらAddTarget()のときに値を渡そうとしてもがいた記憶があります。
今思うと、なんでそんな必要があったのかな〜という気持ちなんですが、
もしかしたらこの記事をみている方も、無理やりセレクタで指定したメソッドに値渡したくてこの記事にたどり着いたかもしれないので、
そのことについて書こうと思います。

結論:セレクタで変数渡したいときはプロパティ経由で渡すよう検討する

結論的にはこれです。
Qiitaを漁ると、userInfoで渡すとか、UIButtonをExtensionで拡張するとか、やり方は色々出てきます。

ただきれいなやり方とは言えないと思います。
ケースバイケースではありますが、クラスなり構造体なりのプロパティとして渡したい変数をつくって、そこ経由で受け渡すのがベストだと思います。
なんらかの理由で渡せないなら、アクセス制御の設計がミスっているので、そこを直した方がいいです。

なぜ初学者はセレクタに変数を渡したがるのか

などと正論を書いても、実際僕も最初の頃セレクタに変数渡さないと実装できなかったシーンが何かあったような記憶がうっすらとあります。
具体的なケースは思い出せないのですが、確かにありました。

なんでだろうな〜と考えたところ、僕の中で一つの結論が出ました。
初学者がセレクタに変数を渡したい、と思うのは、ViewControllerが世界の全てだからではないでしょうか。
Xcodeで新規プロジェクトつくった際に用意されるサンプルコードを使うと、ViewControllerのViewDidLoad()から書き始めることになります。

そこでコード量が多くなって、各処理をメソッドに切り分けていきます。
ViewControllerにはデフォルトだとイニシャライザが書かれていません。
なので、ViewControllerのプロパティを作ろうとすると、イニシャライザがないよ、で怒られます。

初期値を設定してやるとか、オプショナル型にするとか、イニシャライザを真面目に書くとか、
なんらか対処できればいいのですが、いかんせん初学者なので、
「ViewControllerに直でプロパティ書くとなんか上手くいかないなあ……」で終わる(僕の場合ですが)。
となると何がなんでもセレクタに変数を渡せないと、自分がやろうとしている機能ができない! となるわけです。

遠回りのようですが、iOSアプリ全体の構造とか、クラスの使い方とかについて勉強するのが良いのだと思われます。

まとめ

というわけで、SwiftのSelectorについてでした。

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

Flutterでパッケージ名(Package Name / Bundle Identifier)をiOS/Androidで修正する箇所まとめ

はじめに

flutterでプロジェクトを新しく作成するとき、

flutter create my_app_name

とすると、Package Name/Bundle Identifier は自動的に com.example.my_app_name のように指定されてしまいます。リリースに向けて開発を進めるのであればパッケージ名を正しく修正する必要があるので、その修正箇所をまとめておきます。

Android

1. android/app/src/AndroidManifest.xml

  • 3行目: package
  • 8行目: android:label
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.your.orgname.my_app_name">

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="My App Name">

2. android/app/src/debug/AndroidManifest.xml

  • 2行目: package
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.your.orgname.my_app_name">

3. android/app/build.gradle

  • 41行目付近: applicationId
    defaultConfig {
        applicationId "com.your.orgname.my_app_name"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

4. MainActivity.kt

  • 1行目: package
package com.your.orgname.my_app_name

5. ディレクトリ変更

変更前
android/app/src/main/java/com/example/my_app_name

↓ packageに合わせたディレクトリに変更

変更後
android/app/src/main/java/com/your/orgname/my_app_name

iOS

XcodeでBundle Identifierを修正するだけ。

image.png

(※アンダースコアが勝手にハイフンに自動変換がかかりました。Xcodeの仕様のようです。)

最初からorg名を指定するには

flutter create --org com.your.orgname my_app_name

今度からはこのようにcreateしましょう。

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

Flutterでパッケージ名がcom.exampleになっているのをiOS/Androidで修正する箇所まとめ

はじめに

flutterでプロジェクトを新しく作成するとき、

flutter create my_app_name

とすると、Package Name/Bundle Identifier は自動的に com.example.my_app_name のように指定されてしまいます。リリースに向けて開発を進めるのであればパッケージ名を正しく修正する必要があるので、その修正箇所をまとめておきます。

Android

1. android/app/src/AndroidManifest.xml

  • 3行目: package
  • 8行目: android:label
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.your.orgname.my_app_name">

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="My App Name">

2. android/app/src/debug/AndroidManifest.xml

  • 2行目: package
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.your.orgname.my_app_name">

3. android/app/src/profile/AndroidManifest.xml

  • 2行目: package
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.your.orgname.my_app_name">

4. android/app/build.gradle

  • 41行目付近: applicationId
    defaultConfig {
        applicationId "com.your.orgname.my_app_name"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

5. MainActivity.kt

  • 1行目: package
package com.your.orgname.my_app_name

6. ディレクトリ変更

変更前
android/app/src/main/java/com/example/my_app_name

↓ packageに合わせたディレクトリに変更

変更後
android/app/src/main/java/com/your/orgname/my_app_name

iOS

XcodeでBundle Identifierを修正するだけ。

image.png

(※アンダースコアが勝手にハイフンに自動変換がかかりました。Xcodeの仕様のようです。)

最初からorg名を指定するには

flutter create --org com.your.orgname my_app_name

今度からはこのようにcreateしましょう。

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

iOSでビルド時に自動生成するソースコードをGitで管理しないために

はじめに

iOSアプリ開発において、リソースにタイプセーフにアクセスするためのソースコードを自動生成する SwiftGenR.swift のようなツールをよく利用すると思いますが、みなさんはその自動生成されたソースコードは Git 管理下においていますか?

私の開発中のアプリではこれまで Git 管理下においていたのですが、アプリの要件により次のような状況になりました。

  • Build Configuration に応じて自動生成されるコードに違いが出るようになった
  • ソースコード以外にも Build Configuration に応じてアプリにバンドルさせるファイルを切り替える必要性がでてきた

このような状況ですと、Build Configuration によってファイルが変化してしまうため、ビルド時に自動生成するファイルは Git 管理下におかないようにしました。が、その中でつまづきポイントがあったのでメモとして残します。

Git 管理外にすると...

Git コマンドで自動生成ファイルを Git 管理対象外にします。

$ git rm path/to/Generated/file

Xcode プロジェクトツリー上ではそのファイルが赤色で表示され、存在しないことがわかります。
Xcodeプロジェクトツリー/存在しないファイル

そして、自動生成されてもコミット対象にならないよう、.gitignore ファイルに自動生成先ディレクトリを追加しました。

.gitignore
path/to/Generated

この状態でビルドを実行してみたところ、次のようなビルドエラーとなってしまいました。

error: Build input files cannot be found: 'path/to/Generated/Asset.swift', 'path/to/Generated/StoryboardScenes.swift', 'path/to/Generated/Strings.swift', 'path/to/Generated/StoryboardSegues.swift' (in target 'MyApp' from project 'MyApp')

自動生成のタイミングを確認するも...

自動生成のタイミングがソースコードのコンパイル時に間に合っていないのかと思い、Xcode プロジェクトの Build Phases を確認してみましたが、SwiftGen を実行する Run SwiftGenCompile Sources よりも前のタイミングになっていないので問題なさそうです。
Xcodeプロジェクト/Build Phases

プロジェクトツリー上のファイルの存在チェックはこの Build Phases よりも前のタイミングで実施されるのでしょうか?

解決策

いろいろと調べて R.swift リポジトリのこの issue にたどり着きました。

First build always fails using the new build system with Xcode 10 Beta 6

どうやら Build Phases で生成されたファイル等は青果物として明示してあげる必要がありそうです。ということで先程の Run SwiftGenOutput Files に自動生成されるファイルのパスを追加しました。
Xcodeプロジェクト/Build Phases/Run SwiftGen

これにより、自動生成される前の状態でのXcodeプロジェクトツリー上での表示もこのように変わりました!ファイルアイコンは半透明のままですが、ファイル名は赤色ではなくなっています。
Xcodeプロジェクトツリー/存在しないファイル2

もちろん、ビルドも問題なく通るようになりました :thumbsup:

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

Apple Music API(MusicKit)のDeveloper Token取得方法

いつも忘れてググって色んなサイト飛んだりするので、自分の備忘録として書いておく

Apple Developerページでの作業

https://developer.apple.com/account/

Music IDを取得

  1. Developer Accountの証明書等々の一覧ページにいく

  2. Identifiers+ボタンより Music IDsを選択

  3. Description, Identifierを入力し、作成

MusicKit用の秘密鍵を作成

  1. Keysページで+ボタンより作成

  2. Key Nameを入力し、Music Kitconfigureボタンで紐付けたいMusic IDを設定

  3. Registerボタンで作成し、Downloadボタンよりダウンロードしておく。 .p8ファイルがダウンロードされる

Key ID, TeamIDを確認

これに加え、先程作成した.p8 ファイルを用意

apple-music-token-generatorを使用してDeveloper Token作成

apple-music-token-generatorをダウンロードし、READMEに従いPython関連をインストール

  • music_token.pyの内容を自分の秘密鍵, Key ID, Team IDに書き換える

    秘密鍵 ... 先程ダウンロードした.p8ファイルの内容

  • ターミナル上でmusic_token.pyを実行

 python music_token.py
  • 出力されたTokenでApple Music APIにアクセスできる
   curl -v -H 'Authorization: Bearer 出力されたToken' "https://api.music.apple.com/v1/catalog/us/artists/36954" 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

なぜiPhone 11 Pro では0.5の線が描画できないのか調べてみた

環境

  • Xcode 11.3

何が起こったのか

いつもどおり 0.5 の線を引くために Storyboard の AutoLayout で Height = 0.5 の制約を指定した。

image.png

すると、デザイナーから「線の太さにばらつきがあるので揃えてほしい」と指摘があった。

image.png

↑たしかによく見ると上の線のほうが若干濃くも見える・・。両方とも 0.5 で指定しているが、念のため次のコードを書いて height を出力してみた。

print(separator1.frame.size.height) // 上の線
print(separator2.frame.size.height) // 下の線

すると、

0.66666666666666
0.33333333333333

と出力された!

目の錯覚ではなく、たしかに上の線のほうが濃いことがわかった。

なぜ0.5で指定しているのに正しく描画されないか

Twitter にてこの疑問をつぶやいてみたところ、 @kishikawakatsumi さんが回答をくださった。

Frameの小数部分は実行時にPixel alignedになるように丸められるからですね。Scaleをかけて割る、みたいな処理が自動的に入ってます。
https://twitter.com/k_katsumi/status/1224279359529205760

なるほど!

つまり、iPhone 11 Pro のような x3 でレンダリングされるデバイスは 1pt の線が実際には 3px で描画される。pxに小数を含むことはできないため、小数点以下は丸められてしまう。

例)0.5pt で指定された線は1.5pxで描画しようとするが、小数点が丸められ、1px(=0.3333pt)か2px(=0.6666pt)として描画される。

このとき、どう丸められるかはコントロールできない。 このため、太さに差が出てしまっていた!

参考:x2 や x3 についてはこのデバイス一覧表がわかりやすい
https://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutions

どうしたらよいか

やはり同じ悩みを持っている人がいた。
ios - How to create a line width of 0.5 pixels - Stack Overflow

つまり、

  • x3 デバイスでは 0.3 pt で指定するようにする
  • x2 デバイスでは 0.5 pt で指定するようにする

このようにすると、確かにどのデバイスでもばらつきがなく線画描画されるようになった。

しかし、 Storyboard では 0.5 単位でしか height を指定できなので、次のようなコードを書いて線を実装する必要がありそう。

(1.0 / [UIScreen mainScreen].scale)

2020/2/4 追記:
1 / scaleの高さを制約に設定したSeparatorというCustom Viewを作る方法もあるそうです!@IBDesignable
にしておくとStoryboardに置くだけで使える
https://twitter.com/k_katsumi/status/1224503484822671360


なにかおかしなことを書いていたらコメントお願いします :pray:

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

UITableViewCell の高さを UIImage のアスペクト比を元に決定する方法

はじめに

UITableViewCell にセットする画像のアスペクト比が固定ではない場合、セルの高さの決め方を工夫する必要があります。今回はその方法の一つとして、 AutoLayout を用いた解決策を示します。

解決策

final class CustomTableViewCell: UITableViewCell {
     @IBOutlet private weak var mainImageView: UIImageView!

     private var aspectConstraint: NSLayoutConstraint? {
         didSet {
             if let value = oldValue {
                 mainImageView.removeConstraint(value)
             }
             if let constraint = aspectConstraint {
                 mainImageView.addConstraint(constraint)
             }
         }
     }

     func prepare(image: UIImage) {
         setImage(image)
     }

     private func setImage(_ image: UIImage) {
         let aspect = image.size.width / image.size.height

         let constraint = NSLayoutConstraint(
             item: mainImageView as Any,
             attribute: .width,
             relatedBy: .equal,
             toItem: mainImageView,
             attribute: .height,
             multiplier: aspect,
             constant: 0.0
         )
         constraint.priority = UILayoutPriority(rawValue: 999)
         aspectConstraint = constraint

         mainImageView.image = image
     }
 }

aspectConstraint: NSLayoutConstraint? ではセルの再利用を考慮して制約の削除も行っています。また、セルに対して複数回画像がセットされる可能性がある場合(セルのイニシャライズが行われてから破棄されるまでの間に複数回 setImage() が呼ばれる場合)にも対応します。

実際に制約を作成して UIImageView に適応するのが setImage() になります。 priority に関しては、構築している UI に応じて臨機応変に設定してください。

参考文献

Dynamic UIImageView Size Within UITableView

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