2018年1月14日日曜日

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

やりたいこと

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

やり方

let str = "abcdefghijklmn"
let startIndex = str.index(emoji.startIndex, offsetBy: 0) //①
let endIndex = str.index(startIndex, offsetBy: 1) //②
let s = String(str[startIndex..<endIndex]) //③SubStringからStringに直しとく

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

①のoffsetBy:は何文字目かということ。ここでは最初の文字なので0にしてる。

②が①で始まる文字から始めて何文字目を終了位置にするかということ。1文字だけならoffsetBy:に1を指定すればいい。

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

注意点

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

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

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の仕様変更などで細かく変わるかもしれない。
やっぱり縮尺をある程度固定しておくのがいいかねえ?

2017年11月8日水曜日

変なメッセージ: You are currently using version x.xx.x of the SDK.

AdMobを使ってると、時々以下のメッセージがコンソールに出る。
これはSDKが最新版じゃないからアップデートしてちょということなので、そうすればいい。
ただ、書いてあるアドレスはリリースノートなので、実際のダウンロードは
https://developers.google.com/admob/ios/download
から。
まあ書いてあるアドレスのページからも行けるけど。

<Google:HTML> You are currently using version 7.24.1 of the SDK. Please consider updating your SDK to the most recent SDK version to get the latest features and bug fixes. The latest SDK can be downloaded from https://goo.gl/UoiJ8F. A full list of release notes is available at https://developers.google.com/admob/ios/rel-notes.

変なメッセージ:[BoringSSL] Function boringssl_context_get_peer_sct_list: line 1754 received sct extension length is less than sct data length

アプリ作ってたら、エラーもWarningもなく正常に動くのに、以下のようなメッセージが出てる。

[BoringSSL] Function boringssl_context_get_peer_sct_list: line 1754 received sct extension length is less than sct data length

【直訳】
[BoringSSL] 関数boringssl_context_get_peer_sct_list:1754行が受信したsctエクステンションの長さがsctのデータ長より短い

わかんねーよ!
BoringSSLは、オープンソースで開発されているセキュリティ通信プロトコル(SSL)である、OpenSSLのセキュリティや不具合を改善するためにGoogleが出した派生版で、その関係のメッセージ。
Googleの広告システムAdMobを入れとくと出るらしく、特に問題が起きてなければ無視しちゃっていいようだ。