ラベル iOSアプリ開発 の投稿を表示しています。 すべての投稿を表示
ラベル iOSアプリ開発 の投稿を表示しています。 すべての投稿を表示

2019年8月22日木曜日

Sad Face Makerをリリースしました

最近モバイルアプリの個人開発が停止中だったのですが、WWDC2019の刺激を受けて久しぶりに新しいアプリを作りました。



Sad Face Makerという写真の編集アプリです。Twitterの投稿機能について学ぶことができました。こちらはQiitaの記事としてまとめています。

アプリを使うとこのようなGIFアニメーションを簡単に作ることができます。



2019年4月19日金曜日

AR Mini Sketchをリリースしました


ARMiniSketchというアプリをリリースしました。

画像の切り抜きやUIGestureのカスタマイズ方法などを学ぶことができました。
もしダウンロード数が増えて来たらAdMobの成果なども公開していきたいと思っています。

AppStoreには載せられなかったのですが、使い方を紹介したビデオです。


2019年3月18日月曜日

iOSのAdMob(Mobile Ads SDK)でGADInvalidInitializationException

appIDの設定方法が変わっていた


開発途中のアプリにAdMobの広告を入れようと思い、過去のアプリと同じ手順でバナー広告を入れたところ次のようなエラーが起きた。

*** Terminating app due to uncaught exception 'GADInvalidInitializationException', reason: 'The Google Mobile Ads SDK was initialized incorrectly. Google AdMob publishers should follow instructions here: https://googlemobileadssdk.page.link/admob-ios-update-plist to include the AppMeasurement framework, set the -ObjC linker flag, and set GADApplicationIdentifier with a valid App ID. Google Ad Manager publishers should follow instructions here: https://googlemobileadssdk.page.link/ad-manager-ios-update-plist'

公式のページ通りに設定をしているのになぜか上手くいかない。

どうやらSDKのバージョンが上がってappIDの設定方法が変わったらしい。

最新版のバージョンではAppDelegateの起動時にappIDの設定を行うのではなく、Info.plistに記述するとのこと。

※その代わりにAppDelegateの起動時にGADMobileAds.sharedInstance().start(completionHandler: nil)を実行する必要がある


公式ページ の日本語版はまだ説明が追いついていないようで、英語版だとInfo.plistに書くようにとの説明になっていた。

日本語版

英語版


2019年2月17日日曜日

縦横それぞれで拡大縮小できるUIPinchGestureRecognizerの改良

はじめに


前回の投稿で縦横それぞれで拡大縮小できるUIPinchGestureRecognizerを作ったが、次の様な問題点があった。
  • たまにXとYのスケールが無限大になってしまう
  • 拡大縮小→指を離す→拡大縮小を繰り返していると、急にXとYのスケールが変わってしまう

原因


調査してみたところそれぞれ次の様なことが原因であった。


たまにXとYのスケールが無限大になってしまう


これはtouchesBegan()が必ず、touchesMoved()よりも前に認識されるだろうという前提のコードになっており、XとYのスケールの計算時にinitPinchWidthとinitPinchHeightの初期値である0で値を割ってしまっていることが原因であった。touchesBegan()のタイミングでユーザが初めに置いた指の間隔からinitPinchWidthとinitPinchHeightを計算するため、touchesMoved()が先に呼ばれるとこの様な状態に陥ってしまう。

尚、Appleの公式のドキュメントにも下記の様に記載があるが、
When the value of this property is false (the default), views analyze touch events in UITouch.Phase.began and UITouch.Phase.moved in parallel with the receiver.

デフォルトの状態ではUIGestureRecognizerはbegan状態とmoved状態を並行して検知してしまうらしく、touchesBegan()がtouchesMoved()よりも先に呼ばれることを前提としてはいけなかった。


拡大縮小→指を離す→拡大縮小を繰り返していると、急にXとYのスケールが変わってしまう


これはオーバライドしたtouchesBegan()とtouchesMoved()の頭でスーパークラスのメソッドを呼んでしまっていることが原因であった。オーバライドしたメソッドの先頭で呼んでしまうとその時点でtouchイベントが発火して次のイベントが検知されてしまうことになる。そうなると、touchesMoved ()のなかでXとYの間隔を計算しているタイミングで次のtouchesBegan()イベントが発生してしまい、initPinchWidthとinitPinchHeightの値が書き換わり、スケールが狂ってしまっていた。

修正コード


上記2点の不具合を修正したコードは次の通り

class TwoDimentionsPinchGestureRecognizer : UIPinchGestureRecognizer {
    private var initPinchWidth : CGFloat = 0
    private var initPinchHeight : CGFloat = 0
    private var _scaleX : CGFloat = 0
    private var _scaleY : CGFloat = 0
    
    var scaleX : CGFloat {
        get { return _scaleX }
    }
    var scaleY : CGFloat {
        get { return _scaleY }
    }
    
    override func touchesBegan(_ touches: Set, with event: UIEvent) {
        guard touches.count == 2 else { return }
        
        let locations = touches.compactMap { touch in
            return touch.location(in: self.view)
        }
        
        initPinchWidth = abs(locations[0].x - locations[1].x)
        initPinchHeight = abs(locations[0].y - locations[1].y)
        
        super.touchesBegan(touches, with: event)
    }
    
    override func touchesMoved(_ touches: Set, with event: UIEvent) {
        guard touches.count == 2 else { return }
        guard initPinchWidth != 0 else { return }
        guard initPinchHeight != 0 else { return }
        
        let locations = touches.compactMap { touch in
            return touch.location(in: self.view)
        }
        
        let newPinchWidth = abs(locations[0].x - locations[1].x)
        let newPinchHeight = abs(locations[0].y - locations[1].y)
        
        _scaleX = newPinchWidth / initPinchWidth
        _scaleY = newPinchHeight / initPinchHeight
        
        super.touchesMoved(touches, with: event)
    }
    
    override func touchesEnded(_ touches: Set, with event: UIEvent) {
        initPinchWidth = 0
        initPinchHeight = 0
        
        super.touchesEnded(touches, with: event)
    }
 }

2019年2月14日木曜日

縦横それぞれで拡大縮小できるUIPinchGestureRecognizer

はじめに

標準のUIPinchGestureRecognizerだと得られるScaleはタッチした2点の距離をベースに計算されます。そのため、縦に伸ばしたか横に伸ばしたかは結果に反映されません。
縦横それぞれでScaleが欲しかったため、UIPinchGestureRecognizerを拡張してTwoDimentionsPinchGestureRecognizerというクラスを作成しました。

実装と解説

実際のコード

class TwoDimentionsPinchGestureRecognizer : UIPinchGestureRecognizer {
    private var initPinchWidth : Float = 0
    private var initPinchHeight : Float = 0
    private var _scaleX : Float = 0
    private var _scaleY : Float = 0
    
    var scaleX : Float {
        get { return _scaleX }
    }
    var scaleY : Float {
        get { return _scaleY }
    }
    
    override func touchesBegan(_ touches: Set, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        
        guard touches.count == 2 else { return }
        
        let locations = touches.compactMap { touch in
            return touch.location(in: self.view)
        }
        
        initPinchWidth = Float(abs(locations[0].x - locations[1].x))
        initPinchHeight = Float(abs(locations[0].y - locations[1].y))
    }
    
    override func touchesMoved(_ touches: Set, with event: UIEvent) {
        super.touchesMoved(touches, with: event)
        
        guard touches.count == 2 else { return }
        
        let locations = touches.compactMap { touch in
            return touch.location(in: self.view)
        }

        let newPinchWidth = Float(abs(locations[0].x - locations[1].x))
        let newPinchHeight = Float(abs(locations[0].y - locations[1].y))
        
        _scaleX = newPinchWidth / initPinchWidth
        _scaleY = newPinchHeight / initPinchHeight
    }
}

解説

まず、touchesBeganで初回のタッチした2点間の縦横それぞれの距離を計算します。そして、touchesMovedで初回の縦横の距離からどれだけ変わったかの比率を計算して、それぞれscaleXとscaleYとして値を呼び出し元に渡します。
尚、touchesBeganとtouchesMovedで取得されるtouchesはSetのため順番が保証されていません。より完成度を求めるならばcompactMapで得られた結果をソートする必要があります。


TwoDimentionsPinchGestureRecognizerのデモ


2019年2月10日日曜日

UIKitの表示変化をメンバ変数に頼り過ぎてはいけない

はじめに

先日AR Cake Dividerをリリースしましたが、早速バグが見つかりました(汗)
設定画面では設定項目をタップすると、ダイアログを表示し、そこに表示されるPickerで値を選択できる様にしています。そして、設定値は「ガイド形式」と「分割数」と二つ用意しているのですが、交互に表示していると急にアプリが落ちてしまうことがあります。
このバグは私のとりあえずメンバ変数でいいやという安易な考えが招いており、戒めのためにも不具合の原因を説明させて頂きます。

原因

Pickerが表示するリストはそれぞれ別の配列で定義しており、選ばれた設定項目とPickerで選択されたrowの値はViewControllerのメンバ変数として保持しています。
交互にダイアログを操作されるとrowの値はその都度変わりますが、rowの値を元に配列の値を参照しようとしたときに、rowの値が既に書き換えられてしまっており、配列の範囲外の場所を参照してしまいエラーが発生していました。




例えば、上の様に2つのボタンでそれぞれ、別のPickerを表示させ、選択した値でラベルを更新するプログラムがあったとします。


import UIKit

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    @IBOutlet weak var label: UILabel!
    
    enum ButtonId {
        case one
        case two
    }
    
    var selectedButton = ButtonId.one
    var selectedRow = 0
    
    let button1List : [String] = ["apple", "orange", "banana", "grape", "strawberry"]
    let button2List : [String] = ["black", "white"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    @IBAction func pushButton1(_ sender: Any) {
        selectedButton = .one
        showAlert()
    }
    
    @IBAction func pushButton2(_ sender: Any) {
        selectedButton = .two
        showAlert()
    }
    
    func update() {
        switch selectedButton {
        case .one:
            label.text = button1List[selectedRow]
        case .two:
            label.text = button2List[selectedRow]
        default:
            break
        }
    }
    
    private func showAlert() {
        let vc = UIViewController()
        vc.preferredContentSize = CGSize(width: 250,height: 250)
        let pickerView = UIPickerView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
        pickerView.delegate = self
        pickerView.dataSource = self
        vc.view.addSubview(pickerView)
        
        var title = ""
        switch selectedButton {
        case .one:
            title = "fruit"
        case .two:
            title = "color"
        default:
            break
        }
        
        let editAlert = UIAlertController(title: title, message: "", preferredStyle: UIAlertController.Style.alert)
        editAlert.setValue(vc, forKey: "contentViewController")
        editAlert.addAction(UIAlertAction(title: "done", style: .default, handler: { (UIAlertAction) in
            self.selectedRow = pickerView.selectedRow(inComponent: 0)
            self.update()
        }))
        editAlert.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil))
        self.present(editAlert, animated: true)
    }
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch selectedButton {
        case .one:
            return button1List.count
        case .two:
            return button2List.count
        default:
            return 0
        }
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        switch selectedButton {
        case .one:
            return button1List[row]
        case .two:
            return button2List[row]
        default:
            return nil
        }
    }
}


このソースの様に、メンバ変数のselectedRowでPickerの橋渡しを行い、別途update()を呼ぶ様にすると、update()内で配列を参照するタイミングでユーザ操作によりselectedRowの値が書き換えられるとアウトになります。

この様な事を防ぐためにはCで言う"値渡し"でselectedRowの値をupdate()に伝える様にします。

    func update(_ selectedRow : Int) {
        switch selectedButton {
        case .one:
            label.text = button1List[selectedRow]
        case .two:
            label.text = button2List[selectedRow]
        default:
            break
        }
    }
    
    private func showAlert() {
        let vc = UIViewController()
        vc.preferredContentSize = CGSize(width: 250,height: 250)
        let pickerView = UIPickerView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
        pickerView.delegate = self
        pickerView.dataSource = self
        vc.view.addSubview(pickerView)
        
        var title = ""
        switch selectedButton {
        case .one:
            title = "fruit"
        case .two:
            title = "color"
        default:
            break
        }
        
        let editAlert = UIAlertController(title: title, message: "", preferredStyle: UIAlertController.Style.alert)
        editAlert.setValue(vc, forKey: "contentViewController")
        editAlert.addAction(UIAlertAction(title: "done", style: .default, handler: { (UIAlertAction) in
            let selectedRow = pickerView.selectedRow(inComponent: 0)
            self.update(selectedRow)
        }))
        editAlert.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil))
        self.present(editAlert, animated: true)
    }


ついついクラス内でしか使わない値なのだからメンバ変数でいいやという発想になってしまいがちだったので、良い教訓になりました。



2019年2月7日木曜日

AR Cake Divider をリリースしました

この度初めてのiOS向けアプリ AR Cake Divider をリリースしました。
ケーキやピザを等分するときに役に立つアプリです。これまで同じ様なアプリはありましたが、ARKitを使ってガイドを付けるアプリは世界初なはずです。




予算の関係上スクリーンショットはピザになってしまいましたが…、今後ケーキを切ってるところをYouTubeにでもアップしたい思ってます。






2019年2月5日火曜日

ARアプリに必要なターゲットカーソルをBlenderで作る

はじめに

ARアプリを作ると、3Dオブジェクトをどこに置くかをユーザにわかりやすく伝えるためターゲットカーソルが必要になることがあります。
Apple公式のメジャーアプリでも出てくるアレです。


このカーソルは切れ目が入っているためSCNTubeやSCNPlaneを組み合わせて作ることが難しいですが、Blenderを使えば簡単に作れるのでその方法を紹介します。

作成環境

  • macOS 10.14.1
  • Blender 2.79

作成手順

1. Blenderを起動する

Blenderを起動するとイニシャルのプロジェクトで上記のような立方体が表示されます。

2. 立方体とカメラ、ライトを削除する

邪魔なので右のペインから全て削除してしまいましょう。

3. カーソルを原点に持ってくる

Shift + S でカーソルを原点に移動させることができます。

4. 円メッシュを2つ作成する

左のペインの「作成」タブ→円を選択して円を作成します。
そのまま「ツール」タブ→拡大縮小を選択すると円の大きさを調整できます。
これを繰り返し大きい円と少し小さな円を2つ作成します。


5. 2つの円メッシュを統合する

このままだとバラバラで面をはると穴なしの円になってしまうため2つを統合して細長いドーナッツを作ります。
「ツール」タブ→統合を選択します。

6. ドーナッツに面を張る


編集モードに変更して、下のペインのメニューから メッシュ > 面 > 面を張るを選択します。

7. ドーナッツ(輪っか)を削る

このままではただの輪っかなのでターゲットカーソルらしく一部分を削ります。
編集モードのままCを押して削りたい部分の頂点を選択します。
そのまま、メニューの削除→頂点を選択します。
同じ作業を繰り返して3箇所削ります。

8. 中央に小さな円メッシュを張る

中央にカーソルを持っていき先ほどの手順で小さな円メッシュを張ります。

9. 両面表示設定をする

このままだと中央の円と周りの輪っかがそれぞれ裏表逆になることがあるので、裏表無しになるように右側のペインを操作して両面表示をONにします。

10. DAE形式のファイルでエクスポート

Xcodeで扱うためDAE(Collada)形式でエクスポートします。

エクスポートが完了すると下記のようにXcodeで扱えるターゲットカーソルが出来上がります。






2019年1月30日水曜日

ARアプリ開発するのにもPlaygroundが便利

はじめに

今ARKitを使ったiOS向けのアプリの開発をしています。ARKitは3Dゲーム開発のためのフレームワークであるSCNKitの延長線にあるため、よく自分の書いたコードが3D空間で思い通りに動いているか確認をします。
これまでは、都度実機で確認を行なっていたのですが、座標変換などの簡単な確認をするのにもいちいち実機で動かすのは面倒に思っていました。しかし、最近PlaygroundでもSCNKitやSpriteKitを動かせることを知り、これは便利だと思ったので紹介します。

手順

1. Xcodeのメニュー > File > New > Playground...を選択する

2. Gameを選択する


画面の左下のexecuteボタンを押すことでコードが実行できますが、この状態では何も表示されません。

3. 右上の Show the Assistant Editor を押す

4. Assistant Editorを開いた状態でコードを実行する


Assistant Editor上に擬似画面が表示され、SpriteKitのコードが実行されます。

ちゃんとマウスカーソルで画面を押すことでインタラクションイベントも確認することができます。


2016年12月2日金曜日

iOSアプリのNavigationBarにメニューアイコンをつける

やりたいこと

下のスクリーンショットにあるようにNavigationBarにメニューを示すアイコンを表示したい

やり方

  • IoniconsSwiftを使ってアイコン画像をロード
  • ロードした画像を使ってUIBarButtonItemインスタンスを作成
  • インスタンスをnavigationItemにsetLeftBarButtonItemsしてやるだけ

実際のコード

override func viewDidLoad() {
    super.viewDidLoad()
    
    // NavigationBarの高さ × 0.8にアイコンのサイズを指定する
    let barHeight : Int = Int((self.navigationController?.navigationBar.frame.size.height)! * 0.8)
    
    // Ioniconよりメニューアイコン画像をロード
    let menuIcon = UIImage.imageWithIonicon(ionicon.AndroidMenu, color: UIColor.black, iconSize: CGFloat(barHeight), imageSize: CGSize(width: barHeight, height: barHeight))

    // アイコン画像からUIBarButtonItemを生成
    let leftNavEditButtonItem = UIBarButtonItem(image: menuIcon, style: .plain, target: self, action: #selector(MyViewController.doSomething))

    // UIBarButtonItemをセット
    self.navigationItem.setLeftBarButtonItems([leftNavEditButtonItem], animated: true)
}

2016年11月22日火曜日

UIButtonに渡すselfはinit後に渡そうね


最近、Androidを離れてSwiftでiOSアプリを作成していますが、次の点にはまったのでメモ 

やりたいこと

  •  イベントに応じてNavigationBarに動的にボタンを追加したり消したりする 


失敗した例

  • UIBarButtonItemのインスタンスを静的メンバとして保持しておく 
  • イベントに応じて保持しているインスタンスをセットしたりアンセットする 
class MyViewController: UIViewController {
    var rightNavEditButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(MyViewController.test))
    
    func test() {
        print("hogehoge")
    }

    func occurSomeEvent(_ calendar: FSCalendar, didSelect date: Date) {
        self.navigationItem.setRightBarButtonItems([rightNavEditButtonItem!], animated: true)
    }
}

 これだと、UIBarButtonItemをnewしたタイミングで引数に渡しているselfが決まっていない(インスタンス化されていない)ため
コード的にエラーはなくともselectorで指定したメソッドが呼ばれない

 正しい例

  • UIBarButtonItemのインスタンスをインスタンスメンバとして保持しておく
  •  viewDidLoadなどselfが決まっている状態でUIBarButtonItemをnewする
  •  イベントに応じて保持しているインスタンスをセットしたりアンセットする
class MyViewController: UIViewController {
    var rightNavEditButtonItem : UIBarButtonItem?
    
    override func viewDidLoad() {
        rightNavEditButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(MyViewController.test))
    }
    
    func test() {
        print("hogehoge")
    }

    func occurSomeEvent(_ calendar: FSCalendar, didSelect date: Date) {
        self.navigationItem.setRightBarButtonItems([rightNavEditButtonItem!], animated: true)
    }
}