2018年1月31日水曜日

AVAudioPlayerで効果音を連続再生

やりたいこと

ボタンを押すたびにピコーンというSEを鳴らすのだが、高速で連続してボタンを押した場合に音が鳴らないことがある。

解決法

音を重ねて鳴らす場合はAVAudioPlayerをしまうプロパティを配列に入れ、再生が終わるまで保持するなどすればいい。(終わったのを消すのがSwiftではちょっと面倒になったけど)

重ねて鳴らさなくていいならば、前の音が再生中なら音を止め、再生ポイント(currentTime)を先頭に戻してやればいい。

具体的には以下のように。

if correctSound.isPlaying {
      correctSound.stop()
}
correctSound.currentTime = 0
correctSound.play()

2018年1月30日火曜日

AVAudioPlayerで初回再生遅延

問題

ゲームで使う短いサウンドをAVAudioPlayerで鳴らすのだが、初回だけ一瞬再生が遅れてしまう。2回目以降の再生では遅延は起きない。

soundPlayer?.prepareToPlay()
をあらかじめ処理しておいてもダメ。

volume = 0
であらかじめ1度再生させ、直後にvolumeを元に戻してみたら、なぜか音が鳴ってしまう。

解決法

現在不明。

2018年1月14日日曜日

任意の位置の文字を取り出す

やりたいこと

たとえば"abcdefghijklmn"という文字列の中から、任意の位置の文字(文字列)を取り出したい。

やり方

let str = "abcdefghijklmn"
let start = str.index(str.startIndex, offsetBy: 3) //①
let end = str.index(start, offsetBy: 2) //②
let subS = str[start..<end] //③
let s = String(subS) //④

昔のBASICだとRight$、Left$、Mid$とかの関数でちょいちょいできたんだけど、Swiftだとちょっと面倒。
文字を取り出す開始位置、終了位置を決め、それをSwiftのRange(範囲指定)の書き方で指定してやる必要がある。

例として4文字目から2文字「de」を取り出してみる。

①が開始位置、②が終了位置を設定するもの。

①が開始位置の指定。
offsetBy:は何文字目かということ。0オリジンのインデックスなので、4文字目なら3。

②が終了位置の指定。
直前で得た開始位置startを使い、そこから何文字目かという書き方をしている。取り出すのが2文字なのでoffsetBy:に2。
逆に終了位置を元の文字列strの先頭から数えて指定する場合は以下でもいい。
let end = str.index(str.startIndex, offsetBy: 5)
これによってともに「f」の文字の位置がendに入る。

それを
③str[start..<end]
と範囲指定をして実際に取り出す。
新しいfor構文の for i in 0...10 と同じようなやり方だ。[ ] で囲むのがなんか配列っぽい。

終了位置を1つ前にする必要あるけど、当然
str[start...end]
という書き方もできる。

注意点

④ただ使うだけならこれだけでいいんだけど、取り出した文字列はSubStringというものになってて元の文字列を参照しているだけなので、元の文字列(ここではstr)が消えると消えてしまうのだそうだ。
だから独立した文字列にするためString( )で囲んでやってる。

Swift4より前では別の書き方をしていたそうだが、それは知らんのでここでは触れない。

もっと簡単にしてよ

開始/終了位置のindexを作り…ってのはわかるけど、SubStringがどーのってのも含め、もっと簡単にして欲しいよね。
それこそBASICのmid$に相当する関数を自作しておいた方がいい。

なお、先頭から/末尾から3文字取り出すっていうのは
let pre = str.prefix(3)
let suf = str.suffix(3)
のように超簡単。SubStringになってるのかどうかは知らんが。

2018年1月12日金曜日

Anyな変数を使い回す

やりたいこと

ゲームとかのステージごとに、その中身を記述したクラスを独立させたいので、それを管理する変数をAny型で作っておき、それを使い回すようにしたい。

たとえば各ステージの内容をZigzagGame.swiftというステージと、NumberGame.swiftというステージを用意した場合、Storyboardで別のViewControllerにせず、一つのViewController上で切り替えて表示したい。

やり方

ステージを管理する変数をインスタンス変数として作っておく。
optionalにしてnilにできるようにするのが肝。

    var gameStage:Any? = ()
もしくは
    var gameStage:AnyObject?

また、ステージを判断できる変数も。
    var stage = 0


その後、ステージを切り替えて使う際には、前のステージで作った部品を消して、gameStage変数にnilを入れて初期化し、新たなステージのクラスで使う。
nilを入れて初期化しないと、変数の型が前のクラスになってるからエラーになっちゃうからね。
Any(要するにどんな型でも受け付ける)型なので、使う際にas! 〜 として型を明確にしてやらないといけないのがちょっと面倒だけど。

    //ステージ切り替え例
    @IBAction func nextGame(_ sender: Any) {
        if stage == 0 {
            //前のステージクリア
            (gameStage as! ZigzagGame).eyePointer.removeFromSuperview()
            (gameStage as! ZigzagGame).shapeLayer.removeFromSuperlayer()
            gameStage = nil
            //新しいステージ
            gameStage = NumberGame(baseView: self.baseView)
            (gameStage as! NumberGame).makeButtons()
            stage = 1
        } else {
            //前のステージクリア
            //数字のボタンを消す
            for i in 0..<20 {
                (gameStage as! NumberGame).buttonCollection[i].removeFromSuperview()
            }
            gameStage = nil
            //新しいステージ
            gameStage = ZigzagGame(baseView: self.baseView)
            (gameStage as! ZigzagGame).apexCount = 30
            (gameStage as! ZigzagGame).animeDuration = 10
            (gameStage as! ZigzagGame).movePath()
            stage = 0
        }
    }

Any、AnyObject、AnyClass

いろんな型を入れられる変数型が複数ある。
  • Any
    • 整数、関数、構造体などのすべての型を扱える
    • var 宣言時は = () として初期化できる(何が入るかは知らんが)
  • AnyObject
    • クラスのインスタンスのみ扱える
    • var 宣言時は = () として初期化できないようだ
    • 荻原剛志さんの「詳解Swift」では‪Objective-C‬との情報交換で利用するって書いてあるけど、別に他のことに使ってもいいよね。
  • AnyClass
    • AnyObject.type として、「すべてのクラス型を暗黙に適合するためのプロトコル」って解説があるけど、よくわかんねぇっす。
    • ひょっとして変数型じゃないのかな? 関数の引数の型宣言として使って、クラス名を得るのに使うっぽい感じも? まあいいや

これでいいのか知らんが

知識がないので、ゲームステージの切り替えをこんなやり方でやっていいのか知らんけどね。もしかしたら笑われてしまうやり方かもしれませんよ。

Layerに描いた図形を全部消す

やりたいこと

UIViewのLayerに描いたドロー図形を全部クリアしたい。

やり方

Layerをインスタンス変数として持っておき、それをremoveFromSuperLayer()してやればいい。


let shapeLayer = CAShapeLayer()
    

func drawFigure() -> Void {
    let path = UIBezierPath()
    
    path.move(to: CGPoint(x: 384, y: 512))
    path.addLine(to: CGPoint(x: 600, y: 600))
    path.addLine(to: CGPoint(x: 600, y: 200))
    
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.fillColor = UIColor.blue.cgColor
    shapeLayer.path = path.cgPath
    self.view.layer.addSublayer(shapeLayer)
}
    
@IBAction func deleteFigure(_ sender: Any) {
    self.shapeLayer.removeFromSuperlayer()

}

注意点

インスタンス変数として持っておくところが肝。単に
self.view.layer.removeFromSuperlayer()
とやると関係ないLayerがなくなっちゃうから画面が真っ黒になっちゃう。
viewのどのLayerを削除するのか指示するために必要なのだな。

もしかしたら変数として持ってなくても消せるやり方あるのか知らんけどさ。

2017年12月28日木曜日

後からNavigationを追加する場合は

StoryboardでNavigationを使う場合、最初からならUINavigationControllerをドロップしてやればいいけど、ViewControllerに後から追加する場合、追加するのはUINavigationBarじゃなく、UINavigationItemの方だ。

2017年12月22日金曜日

MapViewの縮尺が思いどおりにならない

やりたいこと

地図をタップもしくはピンチして拡大/縮小した縮尺を覚えさせ、次回表示時に同じ縮尺で表示したい。
複数の元データがあり、地図を開くごとにそれぞれ個別の縮尺で表示したい。

やったこと

mapViewの.region.span.latitudeDelta、.region.span.longitudeDeltaの値が地図の縮尺の値(正確には表示範囲の緯度、経度の角度)なので、地図を閉じる際にそれを保存し、開く際に同じ値を設定してやる。

うまくいかないこと

表示されたものがだいぶズームアウトしたものになってしまう。

たとえば地図に以下の値を設定したのに、
latitudeDelta: 0.047710965458634291, longitudeDelta: 0.10232551591900574
地図表示後は以下の値になってしまう🤔
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163

再度地図を開閉するとその値を保存するので、開閉するたびにどんどんズームアウトしてしまう。
最初これはiPad、iPhoneシミュレータ(iPadの擬似iPhone表示含む)で起こった。
そのうち、コードやらConstraintsやらをいじっていたところ、iPadで現象が起こらなくなった。そのかわりiPhoneで今度はズームインするようになった。

原因を推測

アップルのドキュメントによると、設定した縮尺はそのまま利用されるのでなく、指定された領域全体が画面に収まるように、勝手に少しずつズームアウトするらしい。
regionプロパティに割り当てる値(またはsetRegion:animated:メソッドで設定する値)は通常、このプロパティによって最終的に保存される値と同じではありません。領域のスパンを設定すると、表示したい矩形が名目的に定義されるだけでなく、Map View自体の拡大縮小レベルも暗黙的に設定されます。Map Viewは、任意の拡大縮小レベルを表示することができないため、指定された領域を、Map Viewがサポートする拡大縮小レベルに合うように調節しなければなりません。Map Viewは、できる限り画面いっぱいに表示しながら、指定した可視全体を表示できる拡大縮小レベルを選択します。その後、それに応じてregionプロパティを調節します。regionプロパティの値を実際には変更せずに結果の領域を確認するには、Map ViewのregionThatFits:メソッドを使用できます。
毎回値が固定されてればいいけど、ズームアウトされた値を保存して次回使うようにすると、使うたびにどんどんズームアウトされちゃうんだな。

どのタイミングで値が変わる?

どのタイミングで値が変わるのかを調べるため、実行時に順に呼ばれるメソッドを監視したところ、以下の順で呼ばれていることがわかった。
ちなみにviewWillAppearの中から地図の設定のメソッドを呼び出して緯度経度および縮尺を設定している。
  1. viewDidLoad
  2. viewWillAppear←この中で縮尺等設定
  3. mapViewWillStartRenderingMap
  4. mapViewWillStartLoadingMap
  5. mapViewDidFinishLoadingMap
  6. viewDidAppear
  7. mapViewDidFinishRenderingMap
  8. mapViewWillStartLoadingMap
  9. mapViewDidFinishLoadingMap

それぞれの時点での縮尺の値の変化


viewDidLoad 
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163

viewWillAppearから呼び出すメソッド内でmapViewに値を設定。
設定直前 
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163

(ここでmapViewに前回保存したregionごと設定
mapView.region = prev.region!)

設定直後 
latitudeDelta: 0.33684171745417757, longitudeDelta: 0.42023935281952163 
↑なぜか片方だけ値が勝手に変わっている

viewWillAppearでマップに関係ない他の処理をした最後 
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163 
↑なぜか値が正常値に戻る

mapViewWillStartRenderingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402
↑ここでとうとう値が大きく変わり、以下そのまま

mapViewWillStartLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewDidFinishLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

viewDidAppear 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewDidFinishRenderingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewWillStartLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewDidFinishLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

対処方法

  • 縮尺を固定する
    • 前回の縮尺を利用っていう希望は実現されないけど、いちばん楽。
  • 表示時か保存時に値をちょっとずつズームインさせて、調整分を吸収する
    • どれくらいズームインするのかわからないので、結局使っているうちに少しずつズームアウトされたり、ズームインしすぎたりしてしまう恐れがある。
    • 必要なズームインの比率がデバイスの違いや、マップサイズの変更、iOSの仕様変更などで細かく変わるかもしれない。
やっぱり縮尺をある程度固定しておくのがいいかねえ?