2018年3月31日土曜日

Xcode9.3リリース

以下、AppStoreのリリースノートを翻訳

Xcode 9.3には、iOS 11.3、watchOS 4.3、tvOS 11.3、およびmacOS High Sierra 10.13.4用のSwift 4.1およびSDKが含まれています
  • Xcode 9.3には、iOS 11.3、watchOS 4.3、tvOS 11.3、およびmacOS High Sierra 10.13.4用のSwift 4.1およびSDKが含まれています
  • [オーガナイザ]ウィンドウの[新しいエネルギー]タブには、アプリの消費電力が大きすぎるときに生成されるログが含まれます
  • 迅速なビルドタスクと他のコマンドは、ビルドのパフォーマンスを改善するために、より頻繁に並行して実行されます
  • Swiftコンパイラが-Osizeビルド設定で有効になった新しいコードサイズの最適化を追加
  • コマンドキーを押しながらシンボルをクリックすると、発信者にすばやくアクセスできます
  • アセットカタログは、ARKitアプリケーションによって現実世界で検出できるAR参照画像ファイルをサポートします
  • 新しいxccovコマンドラインツールを使用すると、コードカバレッジレポートを検査できます
  • 非常に大きなファイルで作業するときのソースエディタのパフォーマンスの向上
  • 追加のバグ修正と安定性の向上

2018年3月21日水曜日

ScrollViewのContentSize

引っかかって悩んだところ。

ContentSize

上のように、縦横240pixelのScrollViewの上に、300*200のViewを置き、その上にいくつかLabelを置いた。
そのまま実行させると横だけにスクロールして、隠れている横60pixelも見られる。(ScrollViewに直接置いた「WH240のScrollView」というLabelはスクロールしない)

ContentSizeの値を変えてみる

ContentSizeはスクロールさせる中身の大きさということなので、これをviewDidLoad()内で以下のようにそれぞれ変更した場合はどうか?
myScrollView.contentSize = CGSize(width: 250, height: 200)
myScrollView.contentSize = CGSize(width: 500, height: 200)
なぜか何の変化もなく、ちゃんと隠れた横60pixelが表示されたところでスクロールは止まる。
予想では途中までしかスクロールしなかったり、View全体が表示されてもさらにスクロールし続けると思ってたのだが、そうならない。
viewDidAppear()内で設定したところ、期待どおりの動作になった。
これで実際に置かれた中身のコンテンツの大きさに関わらず、スクロール量を調整できるわけだ。

実はコンテンツの一部だけをスクロールさせたかったのに、rootのViewの大きさ分(=画面全体)がスクロールしてしまい、困ってたのだ。

2018年3月8日木曜日

画像の作り直しでJPEGがPNGになっちゃった

Retinaディスプレイの画面をキャプチャし、それをツイッターに投稿するアプリを作ってるが、画像容量がでかくなるので縮小したい。

画面キャプチャだけ

iPhoneだとRetina解像度でちょうどいいのだが、iPadだとファイルサイズが大きくなるためツイートをはねられることがしばしばある。5MB制限らしいので、それをオーバーしてしまうようだ。
うまく投稿できても15秒くらいかかって遅い。投稿できた画像は元と同じくJPEG。

UIGraphicsBeginImageContextWithOptions(realSize, false, 2.0) //Contextを作成(Retina対応で2.0)
let ctx = UIGraphicsGetCurrentContext() //現在のContextを得る
imageView.layer.render(in: UIGraphicsGetCurrentContext()!) //Context部分に表示されている画像その他をレンダリング(ここが画面キャプチャ)
var image = UIGraphicsGetImageFromCurrentImageContext() //UIImageを作る

UIGraphicsEndImageContext()

縦横ピクセルを制限

iPadのRetina解像度のまま投稿すると縦横の長さも大きすぎるので、はみ出してる画像は最大1000ピクセルに縮小させるようコードを書いた。
いっぺんに縮小するのでなく、一度キャプチャして出来た画像を再度GraphicContextを使ってリサイズしている。
Retina解像度でキャプチャしているので、image.sizeで得られるwidth、heightは1/2になっているので注意。最終的に得られる画像はその倍になるのでややこしい。
1000pxの画像を得たいときに500pxに縮小すると最終的に1000pxになるというもの。

//画像縦横サイズ制限
//maxPixelSizeは例えば500とか入ってる
//縦横の最大を1000pxに抑えたいが、2倍精細なRetina解像度なので、image.sizeで得られる解像度はその半分になるため
var newSize = image?.size
if image!.size.width > maxPixelSize || image!.size.height > maxPixelSize {
    if image!.size.width >= image!.size.height {
        let rate = maxPixelSize / image!.size.width //縦横比
        newSize = CGSize(width: maxPixelSize, height: image!.size.height * rate)
    } else {
        let rate = maxPixelSize / image!.size.height //縦横比
        newSize = CGSize(width: image!.size.width * rate, height: maxPixelSize)
    }
        //UIImage作り直し開始。なぜかPNGになってしまう
    UIGraphicsBeginImageContext(newSize!)
    image!.draw(in: CGRect(x: 0, y: 0, width: newSize!.width, height: newSize!.height))
    image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    ⭐️
}

そうしたらなぜか画像がPNGになってしまった。縮小してるから投稿できないことはないが、1MBオーバーで縮小したありがたみが薄い。

キャプチャ時の解像度を非Retinaにするとサイズ制限しなくてもJPEGになってくれるけど、今度はiPhoneでのキャプチャ解像度が低いため、画像が小さすぎてしまう。

しょうがないので、一度JPEGのData型に直し、さらにそれをUIImageに戻して解決した。

上記コードの⭐️に挿入
//0.7は圧縮率
let imageData = UIImageJPEGRepresentation(image!, 0.7)
image = UIImage(data: imageData!)

  1. Retina解像度でContextを作ってキャプチャ
  2. UIImageとして変数に
  3. 縦横を同比率で縮小するためにwidthとheightを計算
  4. 計算したwidth、heightでContextを作り、2.のUIImageをdrawすることで画像縮小(このままだとPNGになる)
  5. UIImageをJPEG化してDataに直す(UIImageのままフォーマット変換できないから)
  6. DataをUIImageに戻して終了

原因はわからないが、UIImageって内部的にJPEGとかPNGとかの情報も持ってるのね?

2018年3月7日水曜日

文字列の長さを調べる

結論

先に言うと、文字列.count で得られる。

詳細

文字列の長さ(文字数)を調べるのに、Swiftでは多くのプログラミング言語にあるlengthなどのプロパティがない。
これはUnicodeの導入で1文字あたりのバイト数のバリエーションが増えたため、一つのlengthで対応できないためのようだ。

そこで以下のようなそれぞれに対応したcountプロパティで得ることになる。
unicodeScalarsはUTF32用だ。

"猫".utf8.count ->3
"🐱".utf8.count ->4

"猫".utf16.count ->1
"🐱".utf16.count -> 2

"猫".unicodeScalars.count ->1
"🐱".unicodeScalars.count ->1

"猫".characters.count ->1

"🐱".characters.count ->1

最後のcharactersが「見た目に何文字か」を返してくれて一番良かったのだが、Swift 4でdeprecatedになり、結局
"猫".count ->1
"🐱".count ->1
で落ち着いたようだ。
これなら楽でいいや。

2018年3月4日日曜日

GameCenterの設定方法

iTunes Connectの処理

iTunes ConnectでGame CenterのスイッチをONにする。

マルチプレーヤー互換性には、今登録しようとしているアプリが入ることになるのだが、他のタブとかを行き来してるとしばらくして勝手に表示してくれる。

上のタブの「機能」、左のタブの「Game Center」に移動。
グループに移動は他のアプリとスコアを共有する場合なので、アプリ単独の場合は無視。
Leaderboard(0) の+を押すと以下の画面に。

シングルLeaderboardを選択する。

Leaderboard

Leaderboardとは、Game Center画面の項目のこと。

  • Leaderboardの参照名
    • Leaderboardごとに付ける内部名。iTunes Connect内で検索する際に使用
  • Leaderboard ID
    • このLeaderboardのための固有の識別子。. や _ も含められる。
    • プログラムの中で使う
    • 一度決めたら変更不可なので注意
    • 自分の他のアプリとも共通なので、アプリごとにユニークにする必要があるようだ
  • スコアのフォーマットタイプ

      • 整数
      • 固定小数 —— 少数点以下1〜3桁
      • 経過時間 —— 分、秒、100分の1秒
      • 金額 —— 整数、小数点以下2桁まで
  • スコア送信タイプ
    • 最初に表示するのをベストスコアにするか、最新スコアにするか
  • 並べ替えの順序
    • 最高のスコア(一番多いスコア)を最初に表示するなら「降順」を。スコアが小さい方が勝ちのゲームなら「昇順」でいいんじゃないかと思う。
  • スコアの範囲(オプション)

  • Leaderboardのローカリゼーション

言語ごとにLeaderboardに表示する「名前」を変える。(例として挙げてるゲームは漢字やエジプト文字を取るゲームなので、日本語でのローカライズ時は「漢字」と表示させる。まぎらわしくてスマン)
スコアのフォーマットは、3桁ごとにカンマ「,」で区切るかピリオド「.」で区切るかとかそんなもの。
スコアのフォーマットサフィックスってのは「〇〇点」などと付ける場合。
画像も付けられる。使ったことないけど。

実際の設定例



Leaderboardを追加

Leaderboardの設定が終わって保存されたら、最初のGame Centerの所にLeaderboard(+)が出てくるので、クリックして設定したものをチェックして加える。

Xcodeの設定

CapabilitiesをONにする

これはGameKit.frameworkのインストールとInfo.plistへの設定追加を自動でしてくれるものなので、次のように自分でやっても同じことだが、こっち推奨。
❗️で警告されてるのはiTunes Connectで設定しろって意味だと思うんで、もう終わってる。iTunesConnectできちんと設定できていればそのうち✔に変わってくれるはずだ。


以下の2つは自分でやる場合。自分でやってもCapabilitiesのON/OFFは変化せずアンマッチになるので、CapabilitiesをONにした方が楽で確実だ。
  1. GameKit.frameworkをインストール
  2. プロジェクトファイルのTARGETS / アプリ名 / Info / Custom iOS Target Properties の Required device capabilitiesにgamekitのstringを追加。
    1. これはInfo.plistと同じなので、直接そちらをいじってもよし。

コード

import GameKit するのは当然。

class のところで GKGameCenterControllerDelegate を追加する。

最低限必要な実装は以下。
  • Game Centerへのログイン
  • Leaderboardなど、ランキングのウィンドウの表示
  • ランキングのウィンドウを消去
  • ハイスコアやチャレンジをクリアしたらGame Centerに情報を送る

ログイン処理

ゲーム開始時に以下のようにGame Centerにログイン処理をする。
このログイン処理は、AppDelegateあたりに書いておいて、他のアプリからアクティブになった時にも機能するようにするといいって。


func authenticateLocalPlayer() {
    let player = GKLocalPlayer.localPlayer() //ログイン確認画面の作成
    player.authenticateHandler = {(viewController, error) -> Void in
        //GameCenterに認証されていない時、viewControllerに認証画面が入ってくるので、
        //それを表示させれば認証処理が簡単にできる
        if viewController != nil
        {
            self.present(viewController!, animated: true, completion: nil)
        }
    }
}

Leaderboard表示と消去

//GameCenterのランキング表示
@IBAction func showRanking(_ sender: Any) {
    let gcView = GKGameCenterViewController()
    gcView.gameCenterDelegate = self
    gcView.viewState = GKGameCenterViewControllerState.leaderboards
    self.present(gcView, animated: true, completion: nil)
}

ログイン後に閉じるdelegate処理も

//Leaderboard処理終了時のdelegate
func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
    //Viewを閉じる
    self.dismiss(animated: true, completion: nil)
}

ハイスコアを送る

以下のようなメソッドを用意しておき、ハイスコア更新時に呼び出す。
引数のIDはiTunes Connectで設定したもの。
rateがスコアになる。Leaderboardの値がInt64なのでそれに合わせてる。

func sendLeaderboardWithID(ID:String, rate:Int64) -> Void {
    //Leaderboard用のインスタンス
    let score = GKScore(leaderboardIdentifier: ID)
    if GKLocalPlayer.localPlayer().isAuthenticated {
        //スコアを設定
        score.value = rate
        print("最高値送信")
        GKScore.report([score], withCompletionHandler: { (error) in
            if error != nil {
                // エラーの場合
                print("GameCenter送信時にエラー \(String(describing: error))")
            }
        })
    } else {
        print("GameCenterにログインしてない!?Σ(((°Д°;))))ガクガク")
    }
}

実機テスト

かつてはiTunes Connectの「ユーザと役割」の「Sandboxテスター」でテスト用アカウントを作る必要があったようだが(実機の設定 / Game Cemter / sandbox をONにする必要もあった)、現在は不要になったそうだ
そのままのアカウントでやるとどうなるのかについての肝心の説明は、Appleのドキュメントからデッドリンクになっててわからん!🤮

参考サイト